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.
Related
I develop a new api with api-platform/core#2.6.6 and I have a problem with my subresource.
I have 2 Entities Project and User:
// App\Entity\Project
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="managedProjects")
* #ORM\JoinColumn(nullable=false)
* #Groups({"project:read", "project:write"})
*/
#[ApiSubresource(
maxDepth: 1,
)]
private User $manager;
/**
* #ORM\ManyToMany(targetEntity=User::class, inversedBy="projects", fetch="EXTRA_LAZY")
* #Groups({"project:read", "project:write"})
*/
#[ApiSubresource(
maxDepth: 1,
)]
private Collection $developers;
On ApiDoc I can see the subresource url :
/api/projects/{id}/manager
/api/projects/{id}/developers
But when I call /api/projects/{id}/developers the request body contains:
{
"#context": "/api/contexts/User",
"#id": "/api/projects/2/manager",
"#type": "hydra:Collection",
"hydra:member": [//...]
}
In the response you can see #id is not good. When I remove the #ApiSubresource on $manager, all works fine.
Any idea?
Thanks for all and happy new year!
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.
I'm using API platform in my Symfony4 app to expose my resources.
It's a great framework but it force you by default to have all your Business logic in the front-end side, because it expose all your Entities and not a Business Object.
I don't like that and I prefer to have my business logic in the back-end side.
I need to create users, but there are different type of users.
So I have create a UserFactory in the back-end-side. So the front just need to push a Business object and the back-end take care of everything.
The front front can never persist a User Object directly in the DB. It is the role of the back-end
Following this tutorial to use DTO for Reading:
https://api-platform.com/docs/core/dto/#how-to-use-a-dto-for-reading
I'm trying to do the same for posting. And it works. Here is my Controller code:
/**
* #Route(
* path="/create/model",
* name="create-model",
* methods={"POST"},
* defaults={
* "_api_respond"=true,
* "_api_normalization_context"={"api_sub_level"=true},
* "_api_swagger_context"={
* "tags"={"User"},
* "summary"="Create a user Model",
* "parameters"={
*
* },
* "responses"={
* "201"={
* "description"="User Model created",
* "schema"={
* "type"="object",
* "properties"={
* "firstName"={"type"="string"},
* "lastName"={"type"="string"},
* "email"={"type"="string"},
* }
* }
* }
* }
* }
* }
* )
* #param Request $request
* #return \App\Entity\User
* #throws \App\Exception\ClassNotFoundException
* #throws \App\Exception\InvalidUserException
*/
public function createModel(Request $request)
{
$model = $this->serializer->deserialize($request->getContent(), Model::class, 'json');
$user = $this->userFactory->create($model);
$this->userRepository->save($user);
return $user;
}
It works great, but I would love my new resource to work in the Swagger UI, so I can Create via POST method new resources directly in the web interface.
For that I think I need to complete the parameter section in my _api_swagger_context. But I don't fin any documentation about that.
How can I do that?
Found the answer here: https://github.com/api-platform/docs/issues/666
You can fill parameters like this :
"parameters" = {
{
"name" = "data",
"in" = "body",
"required" = "true",
"schema" = {
"type" = "object",
"properties" = {
"firstName"={"type"="string"},
"lastName"={"type"="string"},
"email" = {"type" = "string" }
}
},
},
},
More docs about parameters for swagger here : https://swagger.io/docs/specification/2-0/describing-parameters/
I installed SeoBundle and configured the bundle to build a sitemap (docs).
AppKernel.php:
new Sonata\SeoBundle\SonataSeoBundle(),
new Symfony\Cmf\Bundle\CoreBundle\CmfCoreBundle(),
new Symfony\Cmf\Bundle\SeoBundle\CmfSeoBundle(),
Full bundle configurations (config.yml):
sonata_seo:
page:
title: Erasmus internship – Training Experience
metas:
name:
keywords: Erasmus Internships, Internship in Europe, International Internships, Erasmus+, Erasmus Entrepreneur, Student Internships, Internships Abroad, Student Placements
description: Find Internships with Training Experience: Students can find internships & employment opportunities in Europe’s platform for internships. Search paid internships and placements abroad.
viewport: width=device-width, initial-scale=1
format-detection: telephone=no
robots: index, follow
property:
'og:site_name': Training Experience
'og:title': Erasmus internship – Training Experience
'og:description': Find Internships with Training Experience: Students can find internships & employment opportunities in Europe’s platform for internships. Search paid internships and placements abroad."
'og:url': https://www.trainingexperience.org
'og:image': https://www.trainingexperience.org/bundles/index/images/tx-orange.png
http-equiv:
'Content-Type': text/html; charset=utf-8
head:
'xmlns': http://www.w3.org/1999/xhtml
'xmlns:og': http://opengraphprotocol.org/schema/
cmf_seo:
title: seo.title
description: seo.description
sitemap:
enabled: true
content_listener:
enabled: false
Added routes to routing.yml:
sitemaps:
prefix: /sitemaps
resource: "#CmfSeoBundle/Resources/config/routing/sitemap.xml"
Now when I access /sitemaps/sitemap.xml it is opened, but no urls are listed:
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml"></urlset>
What could I be missing?
Important for the items on sitemat is, that they got a matching (allowed, and sitemap name) as a content. Mostly the content isn't loaded. To do so your Content have to implement \Symfony\Cmf\Bundle\SeoBundle\SitemapAwareInterface, which forces you to implement and fill a flag. You can find an example in the tests: SitemapAwareContent:
<?php
/*
* This file is part of the Symfony CMF package.
*
* (c) 2011-2017 Symfony CMF
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Cmf\Bundle\SeoBundle\Tests\Resources\Document;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ODM\PHPCR\Mapping\Annotations as PHPCRODM;
use Symfony\Cmf\Bundle\CoreBundle\Translatable\TranslatableInterface;
use Symfony\Cmf\Bundle\SeoBundle\SitemapAwareInterface;
use Symfony\Cmf\Component\Routing\RouteReferrersReadInterface;
use Symfony\Component\Routing\Route;
/**
* #PHPCRODM\Document(referenceable=true, translator="attribute")
*
* #author Maximilian Berghoff <Maximilian.Berghoff#gmx.de>
*/
class SitemapAwareContent extends ContentBase implements RouteReferrersReadInterface, TranslatableInterface, SitemapAwareInterface
{
/**
* #var string
*
* #PHPCRODM\Locale
*/
protected $locale;
/**
* #var ArrayCollection|Route[]
*
* #PHPCRODM\Referrers(
* referringDocument="Symfony\Cmf\Bundle\RoutingBundle\Doctrine\Phpcr\Route",
* referencedBy="content"
* )
*/
protected $routes;
/**
* #var bool
*
* #PHPCRODM\Field(type="boolean",property="visible_for_sitemap")
*/
private $isVisibleForSitemap;
/**
* #var string
*
* #PHPCRODM\Field(type="string",translated=true)
*/
protected $title;
public function __construct()
{
$this->routes = new ArrayCollection();
}
/**
* #param string $sitemap
*
* #return bool
*/
public function isVisibleInSitemap($sitemap)
{
return $this->isVisibleForSitemap;
}
/**
* #param bool $isVisibleForSitemap
*
* #return SitemapAwareContent
*/
public function setIsVisibleForSitemap($isVisibleForSitemap)
{
$this->isVisibleForSitemap = $isVisibleForSitemap;
return $this;
}
/**
* Add a route to the collection.
*
* #param Route $route
*/
public function addRoute($route)
{
$this->routes->add($route);
}
/**
* Remove a route from the collection.
*
* #param Route $route
*/
public function removeRoute($route)
{
$this->routes->removeElement($route);
}
/**
* Get the routes that point to this content.
*
* #return Route[] Route instances that point to this content
*/
public function getRoutes()
{
return $this->routes;
}
/**
* #return string|bool the locale of this model or false if
* translations are disabled in this project
*/
public function getLocale()
{
return $this->locale;
}
/**
* #param string|bool $locale the local for this model, or false if
* translations are disabled in this project
*/
public function setLocale($locale)
{
$this->locale = $locale;
}
}
you will also see that implementing the interface isn't the only task, you have to set the doctrine mapping also. Doing so, the default loader will fetch you documents and see them (they are visible now).
But you can implement your own loader, voter (another decission item to select) and guesser (to fill in extra data) on your own. So you can decide which content is visible on which (you can have several) sitemap.
The documentation currently shows the process for the loaders, voter and guessers only, so we should insert some hints for the default visibility flag and the default usage at all. So i created an issue. It would be nice to get some feedback there, too.
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.