Serving static files with API platform - symfony

I am creating an API with API platform. One of the features is to be able to upload and download files from a React client developped independently from my API
1 - First try
I followed the docs to setup VichUploaderBundle which led me to the exact same configuration as the docs (https://api-platform.com/docs/core/file-upload/)
From this, I can get my images by sending a GET request to the contentURL attribute set by my subscriber, which has the following format : "localhost/media/{fileName}" .
However, I get a "CORS Missing allow origin" from my app when doing this.
2 - Second try
I fixed this by :
removing the subscriber and the contentUrl attribute
writing an itemOperation on the get method to serve my files directly through the "media_objects/{id}" route :
<?php
// api/src/Controller/GetMediaObjectAction.php
namespace App\Controller;
use App\Entity\MediaObject;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use App\Repository\MediaObjectRepository;
final class GetMediaObjectAction
{
private $mediaObjectRepository;
public function __construct(MediaObjectRepository $mediaObjectRepository)
{
$this->mediaObjectRepository = $mediaObjectRepository;
}
public function __invoke(Request $request): BinaryFileResponse
{
$id = $request->attributes->get('id');
$filePath = $this->mediaObjectRepository->findOneById($id)->getFilePath();
$file = "media/" . $filePath;
return new BinaryFileResponse($file);
}
}
EDIT :
Here is my implementation of the MediaObject entity as requested
<?php
// api/src/Entity/MediaObject.php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use App\Controller\CreateMediaObjectAction;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* #ORM\Entity
* #ApiResource(
* iri="http://schema.org/MediaObject",
* normalizationContext={
* "groups"={"media_object_read"}
* },
* collectionOperations={
* "post"={
* "controller"=CreateMediaObjectAction::class,
* "deserialize"=false,
* "validation_groups"={"Default", "media_object_create"},
* "openapi_context"={
* "requestBody"={
* "content"={
* "multipart/form-data"={
* "schema"={
* "type"="object",
* "properties"={
* "file"={
* "type"="string",
* "format"="binary"
* }
* }
* }
* }
* }
* }
* }
* },
* "get"
* },
* itemOperations={
* "get"
* }
* )
* #Vich\Uploadable
*/
class MediaObject
{
/**
* #var int|null
*
* #ORM\Column(type="integer")
* #ORM\GeneratedValue
* #ORM\Id
*/
protected $id;
/**
* #var string|null
*
* #ApiProperty(iri="http://schema.org/contentUrl")
* #Groups({"media_object_read"})
*/
public $contentUrl;
/**
* #var File|null
*
* #Assert\NotNull(groups={"media_object_create"})
* #Vich\UploadableField(mapping="media_object",fileNameProperty="filePath")
*/
public $file;
/**
* #var string|null
*
* #ORM\Column(nullable=true)
*/
public $filePath;
public function getId(): ?int
{
return $this->id;
}
}
END OF EDIT
Now I don't have this CORS problem anymore since API-platform is directly serving the file when responding to my "media_objects/{id}" route.
However, this brought some questions :
Why did the CORS error pop in the first place ? I would guess it is because when performing a get request directly on the "public" folder, API-platform is not enforcing its CORS policy and not providing the required headers to the client
Is it a correct practice to serve the files this way ? The fact that the documentation introduces a subscriber to create a contentUrl makes me wonder...
Now that the server handles retrieving the file in the Action, does it make sense to have the files in the public folder ? Wouldn't it allow anyone to retrieve my files, and make enforcing security rules on them more difficult ?
Thank you in advance !

Why did the CORS error pop in the first place?
Because API Platform adds the Access-Control-Allow-Origin header to the HTTP response (using Nelmio Cors Bundle) with the CORS_ALLOW_ORIGIN value defined in your .env file. This value typically includes only localhost and example.com by default. The requests send by your React client likely do not originate from either of these hosts, resulting in your browser stepping in and raising an error. More info here.
The Nelmio Cors Bundle configuration documentation explains how to deal with this error. Simplest approach is to set CORS_ALLOW_ORIGIN=* in your .env, and have your nelmio_cors.yaml configuration file include:
nelmio_cors:
defaults:
origin_regex: true
allow_origin: ['%env(CORS_ALLOW_ORIGIN)%']
The generic BinaryFileResponse instance returned by your custom controller doesn't include this header (bypassing all the CORS stuff), resulting in your browser being happy.
Is it a correct practice to serve the files this way?
I recommend sticking with the guidelines and best practices provided by any vendor documentation. This one included.
Does it make sense to have the files in the public folder ? Wouldn't it allow anyone to retrieve my files, and make enforcing security rules on them more difficult?
Nothing wrong with the backend exposing public media assets instead of database blobls. Web servers are very capable of restricting access to these resources if necessary, and so is PHP.

Related

Symfony: datetime property string contraint violation

I am currently learning how to use the Symfony framework. The project that I'm working on is a Web API for a blog application.
Now I have created the necessary entities, provided data into it, set JWT Tokens, etc..
The next step was to automatically set an author (which is currently authorized with the token) to a written blog post. I've added some constraints and other annotations, but when I now use Postman to "POST" a new blog onto the DB it gives me the following error:
{
"title": "Latest Blog Post!",
"published": "2020-08-02 17:00:00",
"content": "This the contentof the latest blog post!",
"slug": "latest-blog-post"
}
Now, the thing is that the property "published" is of type datetime:
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\BlogPostRepository")
* #ApiResource(
* itemOperations={"get"},
* collectionOperations={
* "get",
* "post"={
* "access_control"="is_granted('IS_AUTHENTICATED_FULLY')"
* }
* }
* )
*/
class BlogPost
{
/**
* #ORM\Column(type="datetime")
* #Assert\NotBlank()
* #Assert\DateTime()
*/
private $published;
public function getPublished(): ?\DateTimeInterface
{
return $this->published;
}
public function setPublished(\DateTimeInterface $published): self
{
$this->published = $published;
return $this;
}
}
What am I overlooking here?
Deleted: #Assert\DateTime() and everything worked again properly.

Filter output of a SyliusResourceBundle API call reponse

I'm using the SyliusResourceBundle as a standalone package for exposing data through an API.
When i request entities that have relationships with some other entities, i always get a full response with all the related entities properties included. This leads to heavy JSON responses, and too much data to download on the client side.
Typically, if my entity has a $user property like this :
/**
* #var User
*
* #ORM\ManyToOne(targetEntity="User", inversedBy="object")
*/
private $user;
I get all the user's stuff in the API response when i request the object : name, email, etc.
Is there a way to only get a list of properties/entities i need ? Like with an annotation or something ?
Thanks
For the record, SyliusResourceBundle uses JMSSeriliazerBundle, so it was just a matter of exclusion policy in the Resource Entity.
I just had to exclude all fields at the Entity level, and only expose the field i needed like this :
namespace AppBundle\Entity;
use JMS\Serializer\Annotation as JMS;
use Sylius\Component\Resource\Model\ResourceInterface;
use Doctrine\ORM\Mapping as ORM;
/**
*
* #JMS\ExclusionPolicy("all")
*/
class MyResource implements ResourceInterface
{
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="object")
*/
private $user;
/**
* #ORM\Column(type="string")
* #JMS\Expose()
*/
private $name;
}
See doc here.

Symfony3 + FOSuserBundle : database is already in sync + Authentication request could not be processed

I'm learning how to use FOSuserBundle and I've finished configuring it following the steps from the Symfony docs : Getting Started With FOSUserBundle
The problem is that when I want to update the database schema (doctrine:schema:update --force) I get :
Nothing to update - your database is already in sync with the current entity metadata.
But I think that it should update the user table.
This is my User class :
namespace Stage\AdminBundle\Bundle\Entity;
use FOS\UserBundle\Model\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="fos_user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
// your own logic
}
}
So when I try to login :"Authentication request could not be processed due to a system problem.
" appears.
I don't know what should I do , please help me
thanks
finally i've found the solution , it's a mistake in the namespace
it's :: namespace Stage\AdminBundle\Bundle\Entity;
but it should be namespace Stage\AdminBundle\Entity;

set Nelmio ApiDoc return parameter description

On the ApiDoc for our controller we have specified the output response object and now we see a list of all the parameters that get returned.
How do we provide values for the version and/or description fields on this list?
I have tried adding #ApiDoc(description="text") to the response object's parameters but that doesn't seem to be doing anything.
Thanks in advance.
This is a working API method from one of my projects:
/**
* Get an extended FB token given a normal access_token
*
* #ApiDoc(
* resource=true,
* requirements={
* {
* "name"="access_token",
* "dataType"="string",
* "description"="The FB access token",
* "version" = "1.0"
* }
* },
* views = { "facebook" }
* )
* #Get("/extend/token/{access_token}", name="get_extend_fb_token", options={ "method_prefix" = false }, defaults={"_format"="json"})
*/
public function getExtendTokenAction(Request $request, $access_token)
{
//...
}
All APIDoc parameters that get returned are grouped under "requirements".
I stepped through the ApiDocBundle today and see that Description comes from the comment on the model property or method with #VirtualProperty.
For example:
/**
* This text will be displayed as the response property's description
*
* #var \DateTime
* #JMS\Type("DateTime<'Y-m-d\TH:i:sO'>")
*/
protected $dateTimeProperty;
or
/**
* VirtualProperty comment
*
* #JMS\Type("integer")
* #JMS\VirtualProperty()
* #return integer
*/
public function getVirtualProperty()
{
return $this->someFunc();
}
The same applies to the all comments on the controller method.
I haven't used nelmioApiDoc but looking at the documentation for it, using description="text" in the annotation section seems correct. Have you tried clearing you cache:
php bin/console cache:clear --env=prod
Not sure if it is related.
This section describes how versioning objects is used, and looks like you have to use #Until("x.x.x") and #Since("x.x") in your JMSSerializerBundle classes. See this link.

The method name must start with either findBy or findOneBy! (uncaught exception)

I´ve checked already this but my error seems to be different.
I´m getting this error:
[2012-05-07 14:09:59] request.CRITICAL: BadMethodCallException: Undefined method 'findOperariosordenados'. The method name must start with either findBy or findOneBy! (uncaught exception) at /Users/gitek/www/uda/vendor/doctrine/lib/Doctrine/ORM/EntityRepository.php line 201 [] []
This is my OperarioRepository:
<?php
namespace Gitek\UdaBundle\Entity;
use Doctrine\ORM\EntityRepository;
/**
* OperarioRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class OperarioRepository extends EntityRepository
{
public function findOperariosordenados()
{
$em = $this->getEntityManager();
$consulta = $em->createQuery('SELECT o FROM GitekUdaBundle:Operario o
ORDER BY o.apellidos, o.nombre');
return $consulta->getResult();
}
}
This my controller, where I call the repository:
$em = $this->getDoctrine()->getEntityManager();
$operarios = $em->getRepository('GitekUdaBundle:Operario')->findOperariosordenados();
Finally, this is my Entity:
<?php
namespace Gitek\UdaBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* Gitek\UdaBundle\Entity\Operario
*
* #ORM\Table(name="Operario")
* #ORM\Entity(repositoryClass="Gitek\UdaBundle\Entity\OperarioRepository")
*/
class Operario
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $nombre
*
* #ORM\Column(name="nombre", type="string", length=255)
*/
private $nombre;
----
----
Any help or clue??
Thanks in advance
EDIT: Works fine on dev environment, but no in prod environment.
You already are in a reposoritory, you do not need to re-get it.
All methods in a *Repository can be used as with $this
Also, note
Query Builder or Hand made Query is way too much work when a simple return $this->findBy(); can be used.
findBy() has three parameters, first is an array of relations and getters, the second is for ordering, see Doctrine\ORM\EntityRepository code
Instead of using Raw queries... try the query builder FIRST. Look at my sample.
Your code
I would suggest you simply do:
public function findOperariosordenados()
{
$collection = $this->findBy( array(), array('apellidos','nombre') );
return $collection;
}
You only need EntityRepository
One of my repositories:
Things to note:
Order has a relationship as $owner using the User Entity
If you REALLY need an array, in $array = $reposiroty->getOneUnhandledContainerCreate(Query::HYDRATE_ARRAY)
The ContainerCreateOrder is an extend of Order in a #ORM\InheritanceType("SINGLE_TABLE"). Quite out of scope of this question though.
It could be helpful:
<?php
namespace Client\PortalBundle\Entity\Repository;
# Internal
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Query;
use Doctrine\Common\Collections\ArrayCollection;
# Specific
# Domain objects
# Entities
use Client\PortalBundle\Entity\User;
# Exceptions
/**
* Order Repository
*
*
* Where to create queries to get details
* when starting by this Entity to get info from.
*
* Possible relationship bridges:
* - User $owner Who required the task
*/
class OrderRepository extends EntityRepository
{
private function _findUnhandledOrderQuery($limit = null)
{
$q = $this->createQueryBuilder("o")
->select('o,u')
->leftJoin('o.owner', 'u')
->orderBy('o.created', 'DESC')
->where('o.status = :status')
->setParameter('status',
OrderStatusFlagValues::CREATED
)
;
if (is_numeric($limit))
{
$q->setMaxResults($limit);
}
#die(var_dump( $q->getDQL() ) );
#die(var_dump( $this->_entityName ) );
return $q;
}
/**
* Get all orders and attached status specific to an User
*
* Returns the full Order object with the
* attached relationship with the User entity
* who created it.
*/
public function findAllByOwner(User $owner)
{
return $this->findBy( array('owner'=>$owner->getId()), array('created'=>'DESC') );
}
/**
* Get all orders and attached status specific to an User
*
* Returns the full Order object with the
* attached relationship with the User entity
* who created it.
*/
public function findAll()
{
return $this->findBy( array(), array('created'=>'DESC') );
}
/**
* Get next unhandled order
*
* #return array|null $order
*/
public function getOneUnhandledContainerCreate($hydrate = null)
{
return $this->_findUnhandledOrderQuery(1)
->orderBy('o.created', 'ASC')
->getQuery()
->getOneOrNullResult($hydrate);
}
/**
* Get All Unhandled Container Create
*/
public function getAllUnhandledContainerCreate($hydrate = null)
{
return $this->_findUnhandledOrderQuery()
->orderBy('o.created', 'ASC')
->getQuery()
->getResult($hydrate);
}
}
Did you clear your cache?
php app/console cache:clear --env=prod --no-debug
My app/config/config_prod.yml has a cache driver specified for doctrine :
doctrine:
orm:
metadata_cache_driver: apc
result_cache_driver: apc
query_cache_driver: apc
I cleared APC cache using these function calls :
if (function_exists('apcu_clear_cache')) {
// clear system cache
apcu_clear_cache();
// clear user cache
apcu_clear_cache('user');
}
if (function_exists('apc_clear_cache')) {
// clear system cache
apc_clear_cache();
// clear user cache
apc_clear_cache('user');
// clear opcode cache (on old apc versions)
apc_clear_cache('opcode');
}
And emptied app/cache/ directory.
But I kept getting this error in the prod environment while everything was fine in the dev environment.
I finally rebooted my virtual server and that did the trick.
Which definitely leads me to suspect a cache problem. Next time I will try to (gracefuly) restart the web server only, as that also clears the cache (php - Does a graceful Apache restart clear APC? - Stack Overflow)
Otherwise, setting apc.stat = 1 (http://php.net/manual/en/apc.configuration.php#ini.apc.stat) in /etc/php5/apache2php.ini also seems to be a good idea as suggested here : do we need to restart apache + APC after new version deployment of app?
UPDATE
My development server has APC installed and not APCu. The first two calls to apcu_clear_cache() were causing a PHP Fatal error, which in turn prevented the APC cache from being cleared.
So check which cache your system uses before issuing calls to apcu_clear_cache() or apc_clear_cache(). After that, no need to restart the virtual machine nor the web server to clear the cache and get rid of the nasty exception.
Addded if blocks to run APC or APCu specific functions.

Resources