I wish to limit the routes displayed by the API-Platform generated OpenAPI documentation based on each route's security attribute and the logged on user's roles (i.e. only ROLE_ADMIN can see the OpenApi documentation).
A similar question was earlier asked and this answer partially answered it but not completely:
This isn't supported out of the box (but it would be a nice
contribution). What you can do is to decorate the
DocumentationNormalizer to unset() the paths you don't want to appear
in the OpenAPI documentation.
More information:
https://api-platform.com/docs/core/swagger/#overriding-the-openapi-specification
It appears that DocumentationNormalizer is depreciated and that one should decorate OpenApiFactoryInterface instead.
Attempting to implement, I configured config/services.yaml to decorate OpenApiFactory as shown below.
The first issue is I am unable to "unset() the paths you don't want to appear". The paths exist within the \ApiPlatform\Core\OpenApi\Model\Paths property of \ApiPlatform\Core\OpenApi\OpenApi, but there is only the ability to add additional paths to Paths and not remove them. I've come up with a solution which is shown in the below code which creates new objects and only adds back properties if they do not require admin access, but I suspect that doing so is not the "right way" to do this. Also, I just realized while it removed the documentation from SwaggerUI's routes, it did not remove it from the Schema displayed below the routes.
The second issue is how to determine which paths to display, and I temporarily hardcoded them in my getRemovedPaths() method. First, I will need to add a logon form to the SwaggerUi page so that we know the user's role which is fairly straightforward. Next, however, I will need to obtain the security attributes associated with each route so that I could determine whether a given route should be displayed, however, I have no idea how to do so. I expected the necessary data to be in each ApiPlatform\Core\OpenApi\Model\PathItem, however, there does not appear to be any methods to retrieve it and the properties are private. I also attempted to access the information by using \App\Kernel::getContainer()->get('router'), but was not successful locating the route security attributes.
In summary, how should one prevent routes from being displayed by the API-Platform generated OpenAPI documentation if the user does not have authority to access the route?
config/services.yaml
services:
App\OpenApi\OpenApiFactory:
decorates: 'api_platform.openapi.factory'
arguments: [ '#App\OpenApi\OpenApiFactory.inner' ]
autoconfigure: false
App/OpenApi/OpenApiFactory
<?php
namespace App\OpenApi;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model\Paths;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use App\Kernel;
class OpenApiFactory implements OpenApiFactoryInterface {
private $decorated, $tokenStorage, $kernel;
public function __construct(OpenApiFactoryInterface $decorated, TokenStorageInterface $tokenStorage, Kernel $kernel)
{
$this->decorated = $decorated;
$this->tokenStorage = $tokenStorage;
$this->kernel = $kernel;
}
public function __invoke(array $context = []): OpenApi
{
//$this->debug($context);
$openApi = $this->decorated->__invoke($context);
$removedPaths = $this->getRemovedPaths();
$paths = new Paths;
$pathArray = $openApi->getPaths()->getPaths();
foreach($openApi->getPaths()->getPaths() as $path=>$pathItem) {
if(!isset($removedPaths[$path])) {
// No restrictions
$paths->addPath($path, $pathItem);
}
elseif($removedPaths[$path]!=='*') {
// Remove one or more operation
foreach($removedPaths[$path] as $operation) {
$method = 'with'.ucFirst($operation);
$pathItem = $pathItem->$method(null);
}
$paths->addPath($path, $pathItem);
}
// else don't add this route to the documentation
}
$openApiTest = $openApi->withPaths($paths);
return $openApi->withPaths($paths);
}
private function getRemovedPaths():array
{
/*
Instead of hardcoding removed paths, remove all paths which $user does not have access to based on the route's security attributes and the user's credentials.
This hack returns an array with the path as the key, and either "*" to remove all operations or an array to remove specific operations.
*/
$user = $this->tokenStorage->getToken()->getUser();
return [
'/guids'=>'*', // Remove all operations
'/guids/{guid}'=>'*', // Remove all operations
'/accounts'=>['post'], // Remove only post operation
'/accounts/{uuid}'=>['delete'], // Remove only delete operation
];
}
private function debug(array $context = [])
{
$this->display($context, '$context');
$openApi = $this->decorated->__invoke($context);
$this->displayGetters($openApi);
$pathObject = $openApi->getPaths();
$this->displayGetters($pathObject, null, ['getPath', 'getPaths']);
$pathsArray = $pathObject->getPaths();
$this->display($pathsArray, '$openApi->getPaths()->getPaths()', true);
$pathItem = $pathsArray['/accounts'];
$this->displayGetters($pathItem);
$getGet = $pathItem->getGet();
$this->displayGetters($getGet, '$pathItem->getGet()', ['getResponses']);
$this->display($getGet->getTags(), '$getGet->getTags()');
$this->display($getGet->getParameters(), '$getGet->getParameters()');
$this->display($getGet->getSecurity(), '$getGet->getSecurity()');
$this->display($getGet->getExtensionProperties(), '$getGet->getExtensionProperties()');
$this->displayGetters($this->kernel, null, ['getBundles', 'getBundle']);
$container = $this->kernel->getContainer();
$this->displayGetters($container, null, ['getRemovedIds', 'getParameter', 'get', 'getServiceIds']);
$router = $container->get('router');
$this->displayGetters($router, null, ['getOption']);
$routeCollection = $router->getRouteCollection();
$this->displayGetters($routeCollection, null, ['get']);
$this->displayGetters($this, '$this');
$this->displayGetters($this->decorated, '$this->decorated');
$components = $openApi->getComponents ();
$this->displayGetters($components, null, []);
}
private function displayGetters($obj, ?string $notes=null, array $exclude=[])
{
echo('-----------------------------------------------------------'.PHP_EOL);
if($notes) {
echo($notes.PHP_EOL);
}
echo(get_class($obj).PHP_EOL);
echo('get_object_vars'.PHP_EOL);
print_r(array_keys(get_object_vars($obj)));
echo('get_class_methods'.PHP_EOL);
print_r(get_class_methods($obj));
foreach(get_class_methods($obj) as $method) {
if(substr($method, 0, 3)==='get') {
if(!in_array($method, $exclude)) {
$rs = $obj->$method();
$type = gettype($rs);
switch($type) {
case 'object':
printf('type: %s path: %s method: %s'.PHP_EOL, $type, $method, get_class($rs));
print_r(get_class_methods($rs));
break;
case 'array':
printf('type: %s method: %s'.PHP_EOL, $type, $method);
print_r($rs);
break;
default:
printf('type: %s method: %s, value: %s'.PHP_EOL, $type, $method, $rs);
}
}
else {
echo('Exclude method: '.$method.PHP_EOL);
}
}
}
}
private function display($rs, string $notes, bool $keysOnly = false)
{
echo('-----------------------------------------------------------'.PHP_EOL);
echo($notes.PHP_EOL);
print_r($keysOnly?array_keys($rs):$rs);
}
}
I want to create a TwigExtension Function in Symfony4 in order to display images based on a string property in the view.
I have installed the assets: "symfony/asset": "^4.4",
In lower versions of Symfony I could to this with AssetsHelper::getUrl() -> Returns the public url/path of an asset.
I am not sure how can I achieve this in Symfony4 with "Twig".
The solution is a bit different.
So This is what i did:
In webpackencore config i had to add this in order to have the files copied to the public folder:
.copyFiles({
'from': './assets/img',
'to': 'images/[path][name].[ext]'
})
Then in my extension class the function looks like this:
/**
* #param $type
*
* #return mixed
*/
public function showPaymentTypeIcon($type)
{
$sImagePath = '/build/images/payment-icons';
switch($type) {
case 'card':
default:
$assetImagePath = $sImagePath.'/credit.svg';
break;
case 'sofort':
$assetImagePath = $sImagePath.'/sofort.svg';
break;
}
$oPackage = new Package(new EmptyVersionStrategy());
return $oPackage->getUrl($assetImagePath);
}
So instead of AssetsHelper now we have to use Package as described here: https://symfony.com/doc/current/components/asset.html#asset-packages
Working with Symfony 2.8.24 and PostGIS
I need to THE TITLE. I found this in github but the problem is the configuration steps there didn't help me a lot. I don't know if it is because I can't use composer online (proxy) to install it as the first step states.
IRDK where to start, for example, I don't see the path 'Jsor\Doctrine\PostGIS\Types\GeographyType' anywhere in the files I am using so I don't know where to copy them to use them, or maybe it has nothing to do with it.
I know it's asking a lot but could anyone walk me through it or point me somewhere on how 2 include this library in symfony2.8 from the beginning, I have been using Symfony for a while now (not postgis) and still have a really hard time including this kind of third party libs, so we're talking baby steps here. thanx
Ensure you have the postgis extension installed on your postgresql server.
Install the package via composer.
composer require jsor/doctrine-postgis
Add the types to doctrine-bundle's configuration.
Example:
# app/config/config.yml
doctrine:
orm:
# [..]
entity_managers:
default:
dql:
string_functions:
Geometry: Jsor\Doctrine\PostGIS\Functions\Geometry
Geography: Jsor\Doctrine\PostGIS\Functions\Geography
ST_Distance: Jsor\Doctrine\PostGIS\Functions\ST_Distance
ST_Distance_Sphere: Jsor\Doctrine\PostGIS\Functions\ST_Distance_Sphere
ST_DWithin: Jsor\Doctrine\PostGIS\Functions\ST_DWithin
ST_GeomFromText: Jsor\Doctrine\PostGIS\Functions\ST_GeomFromText
ST_GeographyFromText: Jsor\Doctrine\PostGIS\Functions\ST_GeographyFromText
ST_Transform: Jsor\Doctrine\PostGIS\Functions\ST_Transform
ST_Point: Jsor\Doctrine\PostGIS\Functions\ST_Point
ST_SetSRID: Jsor\Doctrine\PostGIS\Functions\ST_SetSRID
ST_AsEWKT: Jsor\Doctrine\PostGIS\Functions\ST_AsEWKT
Create an entity:
namespace My\Entity;
class GeoLocation
{
protected $latitude;
protected $longitude;
protected $point;
public function __construct($latitude, $longitude)
{
$this->longitude = $longitude;
$this->latitude = $latitude;
$this->setPoint();
}
protected function setPoint()
{
$this->point = sprintf(
'POINT(%f %f)',
(string)$this->longitude,
(string)$this->latitude
);
}
public function getPoint()
{
return $this->point;
}
public function getLatitude()
{
return (float) $this->latitude;
}
public function getLongitude()
{
return (float) $this->longitude;
}
}
Add a mapping for your entity:
My\Entity\GeoLocation:
type: 'entity'
indexes:
idx_point:
columns:
- 'point'
flags:
- 'spatial'
id:
id:
type: integer
generator:
strategy: AUTO
fields:
longitude:
nullable: true
type: 'decimal'
precision: 9
scale: 6
latitude:
nullable: true
type: 'decimal'
precision: 8
scale: 6
point:
nullable: true
type: 'geography'
options:
geometry_type: 'POINT'
srid: 4326
Update your database schema:
app/console doctrine:schema:update [--force]
Now save some points ...
Finally use the postgis types in your doctrine repository:
public function findByDistanceFrom($latitude, $longitude, $distanceInMeters)
{
$qb = $this->createQueryBuilder('geolocation');
$pointFrom = 'Geography(ST_SetSRID(ST_Point(geolocation.longitude, geolocation.latitude), 4326))';
$pointTo = 'Geography(ST_SetSRID(ST_Point(:longitude, :latitude), 4326))';
$qb
->where( $qb->expr()->eq("ST_DWithin($pointFrom, $pointTo, :distance_in_meters)", $qb->expr()->literal(true) ) )
->setParameter('latitude', $latitude)
->setParameter('longitude', $longitude)
->setParameter('distance_in_meters', $distanceInMeters)
;
return $qb
->getQuery()
->getResult()
;
}
I am using the Symfony CMF Media Bundle to achieve the following. I am having several nodes that can have an image and a downloadable PDF.
I have already figured out that the setImage method has to be implemented like that:
public function setPreviewImage($previewImage)
{
if ($previewImage === null) {
return $this;
}
if (!$previewImage instanceof ImageInterface && !$previewImage instanceof UploadedFile) {
$type = is_object($previewImage) ? get_class($previewImage) : gettype($previewImage);
throw new \InvalidArgumentException(sprintf(
'Image is not a valid type, "%s" given.',
$type
));
}
if ($this->previewImage) {
$this->previewImage->copyContentFromFile($previewImage);
} elseif ($previewImage instanceof ImageInterface) {
$previewImage->setName('previewImage');
$this->previewImage = $previewImage;
} else {
$this->previewImage = new Image();
$this->previewImage->copyContentFromFile($previewImage);
}
return $this;
}
Then in another forum someone was suggested to make this property cascade-persistent. with that hint: https://github.com/symfony-cmf/BlockBundle/blob/master/Resources/config/doctrine-phpcr/ImagineBlock.phpcr.xml#L22. Now i am wondering how and were i can set this option in my configuration.
The next part i am wondering about is the cmf_media_file type. Has anyone out here ever managed to store a PDF into a PHPCR node property?
For any help i would be really thankful.
I figured it out by myself.
For anyone who is using annotations you have to set it up like this:
use Symfony\Cmf\Bundle\MediaBundle\Doctrine\Phpcr\Image;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCR;
/**
* #var Image
* #PHPCR\Child(cascade="persist")
*/
The Symfony2 PunkAve FileUpload Bundle works, but because of the returns inside the UploadHandler of BlueImp, it is not possible to get the filename.
<?php
/**
*
* #Route("/upload")
* #Template()
*/
public function uploadAction(Request $request)
{
$editId = $this->getRequest()->get('editId');
if (!preg_match('/^\d+$/', $editId))
{
throw new Exception("Bad edit id");
}
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('Foobar:Foobar')->find($editId);
$destinationFolder = 'test';
$fileUploader = $this->get('punk_ave.file_uploader');
$imageName = $fileUploader->handleFileUpload(array('folder' => $destinationFolder ));
$imageEntity = new \Foobar\Entity\Image();
$imageEntity->setImage($imageName);
$imageEntity->setFolder($destinationFolder);
$em->persist($media);
$em->flush();
return true;
}
The example above uploads the image.
The variable $imageName triggers the fileUploadHandler. There is somewhere a return, why it doesn't go the the next lines where it should save the imagename.
How can I still get it working in Symfony? To save the filename in the Entity after he handled the upload?
As they said in documentation: handleFileUpload DOES NOT RETURN as the response is generated in native PHP by BlueImp's UploadHandler class. handleFileUpload has exit(0); at the end so when you call it then entire process stops there. If you want to save files to database you should do it in action which handles request (from documentation's example it will be editAction) and there, again as documentation said, use getFiles to get the list of filenames and mirror that in your database as you see fit.