Set requirements for get request parameters in symfony's controller - symfony

I have a controller which handles a GET request. I need to set requirement parameters for GET request, e.g.: 'http://localhost/site/main?id=10&sort=asc
My controller class
class IndexController extends Controller {
` /**
* #Route
* (
* "/site/main",
* name="main"
* )
*
* #Method("GET")
*/
public function mainAction(Request $request)
{
return new Response('', 200);
}
}
How could I do that?
UPD: I need to set requirement for URL parameters like
id: "\d+",
sort: "\w+"
Etc.
The same as symfony allows to do with POST request.

You can specify the requirements in the "#Route" annotation like this:
class IndexController extends Controller {
` /**
* #Route
* (
* "/site/main",
* name="main",
* requirements={
* "id": "\d+",
* "sort": "\w+"
* })
* )
*
* #Method("GET")
*/
public function mainAction(Request $request)
{
return new Response('', 200);
}
}

#Method is what you need http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html#route-method
If you try to use this route with POST, you will have 404

I couldn't understand your question well.
However, if what you need is to set up a filter mechanism for the GET method parameters as it is already available for the URL using route requirements, I think there is no ready to use tools for this in the Route component, as commented #Yoshi.
I had to do this kind of work myself and used this. I hope it helps you too
public function indexAction(Request $request)
{
// Parameter names used in the current request
$current_request_params=array_keys($request->query->all());
// $ALLOWED_INDEX_PARAMS should be declared as Class static property array and hold names of the query parameters you want to allow for this method/action
$unallowed_request_params=array_diff($current_request_params,PersonController::$ALLOWED_INDEX_PARAMS);
if (!empty($unallowed_request_params))
{
$result=array("error"=>sprintf("Unknown parameters: %s. PLease check the API documentation for more details.",implode($unallowed_request_params,", ")));
$jsonRsp=$this->get("serializer")->serialize($result,"json");
return new Response($jsonRsp,Response::HTTP_BAD_REQUEST,array("Content-Type"=>"application/json"));
}
// We are sure all parameters are correct, process the query job ..
}

Related

How to assign an array from request to a class property with ParamConverter in Symfony 5

I use Symfony 5 and have a trouble with the ParamConverter bundle when I try to map request to my DTO class.
In my controller I use create method:
/**
* #Rest\Post("/project/{projectId}/blogger-mix/")
* #ParamConverter("command", class=CreateBloggerMixCommand::class, converter="fos_rest.request_body")
*/
public function create(string $projectId, CreateBloggerMixCommand $command, CreateBloggerMixHandler $handler): View
{
$command->projectId = $projectId;
try {
$bloggerSetItem = $handler->handle($command);
return new View(['id' => $bloggerSetItem->getId()], Response::HTTP_OK);
} catch (\Throwable $exception){
return $this->handleErrors($exception);
}
}
The DTO looks like:
class CreateBloggerMixCommand implements CommandInterface
{
/**
* #var array|string[]
*/
public array $bloggerSetItems;
}
When I send request with an array:
{
"bloggerSetItems": [
"f04a76e0-d70e-41df-a926-e180c78b34fc",
"07f6d304-9c97-41e9-8f2d-4a993019280c"
]
}
I receive an error:
{
"success": false,
"status": 500,
"errors": "You must define a type for App\\Project\\Api\\Command\\CreateBloggerMix\\CreateBloggerMixCommand::$bloggerSetItems."
}
In a nutshell, I can't figure out why ParamConverter can't resolve property array. If I change array to string, then ParamConverter responds that can't convert an array to a string it means that ParamConverter eventualy can see the property, but can't resolve array exactly...
Any idea welcome!
Thanx to our partner we found the following solution:
In DTO add:
use JMS\Serializer\Annotation as Serializer;
And in annotation:
/**
* #Assert\NotBlank
* #Assert\Type (type="array")
* #Serializer\Type(name="array<string>")
* #var array|string[]
*/
public array $bloggerSetItems;
I think "You must define a type" refers to database column type. You should just need to add the column definition and then update the database. There are three options for storing an array, choose one:
/**
* #var array|string[]
*
* #Column(type="array") // one of these lines
* #Column(type="simple_array") // one of these lines
* #Column(type="json_array") // one of these lines
*/
public array $bloggerSetItems;
https://www.doctrine-project.org/projects/doctrine-orm/en/latest/reference/basic-mapping.html#doctrine-mapping-types

How to filter custom property on api-platform entity

I have an entity in Api Platform such as a team:
<?php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\Basketball\TeamRepository")
* #ApiResource(
* routePrefix="/v2",
* normalizationContext={"groups"={"public:read"}, "enable_max_depth"=true},
* iri="http://schema.org/Team",
* collectionOperations={
* "get",
* },
* itemOperations={
* "get",
* },
* )
*/
class Team implements ObjectManagerAware
{
use \App\Entity\Traits\UUIDTrait;
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Basketball\Event", mappedBy="teams")
*/
private $events;
public function __construct()
{
$this->events = new ArrayCollection();
}
/**
* #return Collection|Event[]
*/
public function getEvents(): Collection
{
return $this->events;
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events[] = $event;
$event->addTeam($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->contains($event)) {
$this->events->removeElement($event);
$event->removeTeam($this);
}
return $this;
}
}
This all works and I can load teams and events. What I'd like to do is have a property on the API result that returns only upcoming events (not past events). What is the "proper" way to do that? I've tried adding a custom method such as
/**
* #return Collection|Event
* #Groups({"public:read"})
*/
public function getRemainingEvents(): ?Collection
{
$qb = $this->em->createQueryBuilder();
$qb->select('e')->from(Event::class, 'e');
$qb->where(
$qb->expr()->orX($qb->expr()->eq('IDENTITY(e.home)', $this->getId()), $qb->expr()->eq('IDENTITY(e.away)', $this->getId()))
);
$qb->andWhere('e.startDate >= :d');
$qb->andWhere('e.startDate >= :d');
$qb->setParameter('d', (new \DateTime())->format('Y-m-d'));
$qb->orderBy('e.startDate', 'ASC');
$query = $qb->getQuery();
return new ArrayCollection($query->getResult());
}
which does do sort of what I want but the api result using application/vnd.api+json doesn't list the events in the "relationships" the same way that the "events" property does. I've tried looking at the custom filters (https://api-platform.com/docs/core/filters/#creating-custom-filters) but don't understand how that would work for a custom method on the entity.
I'd like the remainingEvents to act the same as events and list the related items in the "relationships" of the response.
Is there some way to annotate it for the serializer or use the filter?
I think you realize that using that approach (a method within an Entity retrieving data from Database) is wrong. Entities should be dummy.
About what are you trying to do, I think your best solution should be adding a DateFilter (https://api-platform.com/docs/core/filters/#date-filter) to the "Events" Resource, allowing you to do a GET events?startDate[after]=2019-09-18
And if you want to filter Events from a specific "Team" Resource, simply add another filter to the Event resource, a Search Filter (https://api-platform.com/docs/core/filters/#search-filter) to specify the Team, allowing you to do a:
GET events?startDate[after]=2019-09-18&team=IRIofteam_OR_ID

Route #Method annotation doesn't seem to get respected when matching routes

I understand when allowing similarly accessible routes, that the order of the routes matter.
Where I'm confused is why when submitting a DELETE request to this route, does it match to the GET route, instead of ignoring it and trying the matched method one below it?
/**
* #Route("/{game}")
* #Method({"GET"})
*/
public function single(Request $request, GameSerializer $gameSerializer, Game $game) {
$out = $gameSerializer->bind($game);
return new JsonResponse($out);
}
/**
* #Route("/{game}")
* #Method({"DELETE"})
*/
public function remove(Request $request, Game $game) {
$em = $this->getDoctrine()->getManager();
$em->remove($game);
$em->flush();
return new JsonResponse([], 200);
}
Full disclosure
I understand why it matches the top most route based on strictly patterns
I dont understand why the access method is getting ignored when doing so
So, just to test, I adjusted to move the DELETE based route up above the GET route
/**
* #Route("/{game}")
* #Method({"DELETE"})
*/
public function remove(Request $request, Game $game) {
$em = $this->getDoctrine()->getManager();
$em->remove($game);
$em->flush();
return new JsonResponse([], 200);
}
/**
* #Route("/{game}")
* #Method({"GET"})
*/
public function single(Request $request, GameSerializer $gameSerializer, Game $game) {
$out = $gameSerializer->bind($game);
return new JsonResponse($out);
}
only.. for this to happen when I tried getting an existing non-test record by performing a basic operation of visiting the url in a browser (so, GET)
and oh boy, did it ever delete that record.
Why is the Access Method being ignored?
First of all, careful of which SensioFrameworkExtraBundle version you are using because the #Method annotation from SensioFrameworkExtraBundle has been removed in latest version. Instead, the Symfony #Route annotation defines a methods option to restrict the HTTP methods of the route:
*
* #Route("/show/{id}", methods={"GET","HEAD"})
*
But in your case, if you're using HTML forms and HTTP methods other than GET and POST, you'll need to include a _method parameter to fake the HTTP method.
See How to Change the Action and Method of a Form for more information.
I think you have to add route name and it must be unique.
Try with following way:
/**
* #Route("/{game}",name="api_remove")
* #Method({"DELETE"})
*/
public function remove(Request $request, Game $game) {
...
}
/**
* #Route("/{game}",name="single_remove")
* #Method({"GET"})
*/
public function single(Request $request, GameSerializer $gameSerializer, Game $game) {
...
}

How to validate data in a custom controler

I created a Entity with a custom contoller:
// api/src/Entity/UserRegistration.php
namespace App\Entity;
use ...
/**
* UserRegistraion Data
*
* #ApiResource(collectionOperations={},itemOperations={"post"={
* "method"="POST",
* "path"="/register",
* "controller"=CreateUser::class}})
*
*/
class UserRegistration
{
.....
/**
* #var string The E-mail
*
* #Assert\NotBlank
* #Assert\Email(
* message = "The email '{{ value }}' is not a valid email.",
* checkMX = true
* )
*/
public $email;
.....
And a custom Controller:
// api/src/Controller/CreateUser.php
class CreateUser
{
.....
public function __invoke(UserRegistration $data): UserRegistration
{
return $data;
}
}
When I call the controller with wrong data (e.g wrong email-address) I would expect an validation error, but it is not checked.
Is there a way to do this?
Api Platform does the validation on the result of your controller, to make sure your data persisters will receive the right information. Thus you may get invalid data when entering your controller, and need to perform the validation manually if your action needs a valid object.
The most common approaches are either using a Form, which provides among other things validation, or just the Validator as a standalone component. In your case you - since are using ApiPlatform - the latter would be the better choice as you don't need to render a form back to the user, but instead return an error response.
First you will need to inject the Validator into your Controller:
use ApiPlatform\Core\Bridge\Symfony\Validator\Exception\ValidationException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
class CreateUser
{
private $validator;
public function __construct(ValidatorInterface $validator)
{
$this->validator = $validator;
}
public function __invoke(UserRegistration $data): UserRegistration
{
$errors = $this->validator->validate($data);
if (count($errors) > 0) {
throw new ValidationException($errors);
}
return $data;
}
}
You can also check how ApiPlatform does it by looking at the ValidateListener. It provides some additional features, e.g. for validation groups, which you don't seem to need at this point, but might be interesting later. ApiPlatform will then use its ValidationExceptionListener to react on the Exception you throw and render it appropriately.

Symfony twig path() when using multiple domains

I have a Symfony project that uses two different domains for two different countries. The site is a client login interface and is basically the same for both domains except for branding and minor name differences. That part is okay.
The two domains are defined in my parameters as us_domain and ca_domain, and in my routing.yml I have:
clients:
resource: #ClientsBundle/Resources/config/routing.yml
host: "clients.{domain}"
prefix: /
requirements:
domain: %us_domain%|%ca_domain%
defaults: { domain: "%us_domain%" }
In Twig, I have my menu using:
<li><span>Home</span></li>
The problem is that although the page will come up on either domain, the paths being generated always use the us_domain, apparently pulling it from defaults in my routing. (I can switch this to ca_domain and the paths do switch).
My question is, why isn't the current domain being detected and used? It seems like the default should be overridden by whatever domain is actually being used?
I'm running Nginx if that matters.
I somehow had missed that the routing variables weren't going to be auto-detected, but you had to pass them as part of your calls. I ended up solving this with a custom Twig filter.
Where I used my path, I passed the domain variable pulling from app.request.host, such as
<li><span>Home</span></li>
And then just wrote Twig extension that parsed/evaluated the host and passed back the domain
<?php
namespace AppBundle\Twig;
use JMS\DiExtraBundle\Annotation as DI;
/**
* Class TwigUrlExtension
* #DI\Service("twig.extension.url")
* #DI\Tag("twig.extension")
*
*/
class TwigUrlExtension extends \Twig_Extension
{
protected $ca_domain;
protected $us_domain;
/**
*
* #DI\InjectParams({
* "us_domain" = #DI\Inject("%us_domain%"),
* "ca_domain" = #DI\Inject("%ca_domain%")
* })
* #param $us_domain
* #param $ca_domain
*/
public function __construct($us_domain, $ca_domain)
{
$this->us_domain = $us_domain;
$this->ca_domain = $ca_domain;
}
/**
* {#inheritdoc}
*/
public function getFunctions() {
return array(
'domainFromHost' => new \Twig_Function_Method($this, 'toDomain')
);
}
/**
* #param string $string
* #return int
*/
public function toDomain ($string) {
if(stripos($string,$this->ca_domain) !== false)
return $this->ca_domain;
if(stripos($string,$this->us_domain) !== false)
return $this->us_domain;
return $string;
}
/**
* {#inheritdoc}
*/
public function getName() {
return 'twig_url_extension';
}
}

Resources