Is there any solution to define global defaults for all routes in a Symfony project? For example, I would like to use the GET method by default for all routes, to avoid routes matching ANY method if I forgot to use methods={…} in one of them.
A less expensive alternative to define all routes with the GET method is to add it to the controller. Now all routes within this controller only allow GET calls unless another one is specified:
/**
* #Method({"GET"})
*/
class MyController extends Controller
{
public function indexAction(Request $request)
{
// only allows GET
}
/**
* #Method({"GET", "POST"})
*/
public function editAction(Request $request)
{
// allows GET and POST
}
}
Related
I am adherent of Action Class approach using instead of Controller. The explanation is very simple: very often Controller includes many actions, when following the Dependency Injection principle we must pass all required dependencies to a constructor and this makes a situation when the Controller has a huge number of dependencies, but in the certain moment of time (e.g. request) we use only some dependencies. It's hard to maintain and test that spaghetti code.
To clarify, I've already used to work with that approach in Zend Framework 2, but there it's named Middleware. I've found something similar in API-Platform, where they also use Action class instead of Controller, but the problem is that I don't know how to cook it.
UPD:
How can I obtain the next Action Class and replace standard Controller and which configuration I should add in regular Symfony project?
<?php
declare(strict_types=1);
namespace App\Action\Product;
use App\Entity\Product;
use Doctrine\ORM\EntityManager;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SoftDeleteAction
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* #param EntityManager $entityManager
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #Route(
* name="app_product_delete",
* path="products/{id}/delete"
* )
*
* #Method("DELETE")
*
* #param Product $product
*
* #return Response
*/
public function __invoke(Request $request, $id): Response
{
$product = $this->entityManager->find(Product::class, $id);
$product->delete();
$this->entityManager->flush();
return new Response('', 204);
}
}
The question is a bit vague for stackoverflow though it's also a bit interesting. So here are some configure details.
Start with an out of the box S4 skeleton project:
symfony new --version=lts s4api
cd s4api
bin/console --version # 4.4.11
composer require orm-pack
Add the SoftDeleteAction
namespace App\Action\Product;
class SoftDeleteAction
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function __invoke(Request $request, int $id) : Response
{
return new Response('Product ' . $id);
}
}
And define the route:
# config/routes.yaml
app_product_delete:
path: /products/{id}/delete
controller: App\Action\Product\SoftDeleteAction
At this point the wiring is almost complete. If you go to the url you get:
The controller for URI "/products/42/delete" is not callable:
The reason is that services are private by default. Normally you would extend from AbstractController which takes care of making the service public but in this case the quickest approach is to just tag the action as a controller:
# config/services.yaml
App\Action\Product\SoftDeleteAction:
tags: ['controller.service_arguments']
At this point you should have a working wired up action.
There of course many variations and a few more details. You will want to restrict the route to POST or fake DELETE.
You might also consider adding an empty ControllerServiceArgumentsInterface and then using the services instanceof functionality to apply the controller tag so you no longer need to manually define your controller services.
But this should be enough to get you started.
The approach I was trying to implement is named as ADR pattern (Action-Domain-Responder) and Symfony has already supported this started from 3.3 version. You can refer to it as Invokable Controllers.
From official docs:
Controllers can also define a single action using the __invoke() method, which is a common practice when following the ADR pattern (Action-Domain-Responder):
// src/Controller/Hello.php
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
/**
* #Route("/hello/{name}", name="hello")
*/
class Hello
{
public function __invoke($name = 'World')
{
return new Response(sprintf('Hello %s!', $name));
}
}
I have 2 services, BlueWorkerService and YellowWorkerService, both implementing the same interface, WorkerServiceInterface. Each of these services use the same entities but with different required logic.
I need to inject one of, but not both, of these classes and use them in ProcessorService so that the interface methods are called using on correct Worker. Which worker service to use is dependent on which Worker is currently being processed. I'll break it down:
Class WorkerProcessor {
private $workerService;
public function __construct(WorkerServiceInterface $workerServiceInterface)
{
$this->workerService = $workerServiceInterface;
}
public function getMixedColourWithRed() {
return $this->workerService->mixWithRed();
}
}
The worker service that is being used would be based on whether the worker being processed has the colour property of Blue or Yellow.
I know I can probably use a Factory to achieve this as described here but my problem is how to tell the factory which Worker colour I am processing?
Running on Symfony 3.4
If you need more info, just ask and I will update the question.
NOTE: I'm using Symfony 4.3.1. I'll post it like that, then I'll help you to move all code from this architecture to Symfony 3.4.
I'm using a similar concept to load different classes in my project. Let me explain first, then I'll add code under this text.
Firstly, I'm loading a custom compiler pass under src/Kernel.php (your file is app/AppKernel.php):
/**
* {#inheritDoc}
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new BannerManagerPass());
}
BannerManagerPass its created under src/DependencyInjection/Compiler (in your case should be src/BUNDLE/DependencyInjection/Compiler`).
class BannerManagerPass implements CompilerPassInterface
{
/**
* {#inheritDoc}
*/
public function process(ContainerBuilder $container)
{
if (!$container->has(BannerManager::class)) {
return;
}
$definition = $container->findDefinition(BannerManager::class);
$taggedServices = $container->findTaggedServiceIds('banner.process_banners');
foreach (array_keys($taggedServices) as $id) {
$definition->addMethodCall('addBannerType', [new Reference($id)]);
}
}
}
As you see, this class should implement CompilerPassInterface. You can observe that I'm looking for specific services tagged as banner.process_banners. I'll show how I tagged services a little bit later. Then, I'm calling addBannerType method from BannerManager.
App\Service\BannerManager.php: (in your case src/BUNDLE/Service/BannerManager.php)
class BannerManager
{
/**
* #var array
*/
private $bannerTypes = [];
/**
* #param BannerInterface $banner
*/
public function addBannerType(BannerInterface $banner)
{
$this->bannerTypes[$banner->getType()] = $banner;
}
/**
* #param string $type
*
* #return BannerInterface|null
*/
public function getBannerType(string $type)
{
if (!array_key_exists($type, $this->bannerTypes)) {
return null;
}
return $this->bannerTypes[$type];
}
/**
* Process request and return banner.
*
* #param string $type
* #param Server $server
* #param Request $request
*
* #return Response
*/
public function process(string $type, Server $server, Request $request)
{
return $this->getBannerType($type)->process($request, $server);
}
}
This class has a custom method (created by me) called process(). You can name it whatever you want it, but I think that's pretty verbose. All parameters are sent by me, so don't mind. You can send whatever you want.
Now we have our Manager and compiler pass is set. It's time to set our banner types (based on my example) and tag them!
My banner types are under src/Service/Banner/Types (in your case should be src/BUNDLE/Service/WhateverYouWant/Type. This does not matter! You can change it later from services.yaml).
These types are implementing my BannerInterface. It does not matter the code under the class in this instance. One more thing that I should warn you! You should see that under BannerManager, inside the addBannerType() I'm calling $banner->getType(). This is one method inherited from BannerInterface in my case and it has a unique string (in my example I have three banner types: small, normal, large). This method can have any name, but don't forget to update it as well in your manager.
We are almost ready! We should tag them, then we are ready to try them!
Go to your services.yaml and add these lines:
App\Service\Banner\Types\:
resource: '../src/Service/Banner/Types/'
tags: [banner.process_banners]
Please see the tag!
Whatever I want to show a custom banner, I'm using a simple URL with $_GET where I keep my banner type, then I load it like this:
public function view(?Server $server, Request $request, BannerManager $bannerManager)
{
...
return $bannerManager->getBannerType($request->query->get('slug'))->process($request, $server);
}
I am using the FOSRestBundle to create a REST application but since REST features is only a part, I am also using some of Symfony2 built-in automation tools to generate my CRUD code. Everything works fine but I am unable to correctly map the route and I will appreciate some insight and example on how to do this manually. I have read the manual route definition in the FOS manual stating to use the given annotations but how do I do this since the CRUD code created by Symfony2 uses a different annotation?
Here is an example:
class UserController extends Controller
{
/**
* Lists all User entities.
*
* #Route("/", name="user")
* #Method("GET")
* #Template()
*/
public function indexAction()
{
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('CompanyWebServicesBundle:User')->findAll();
return array(
'entities' => $entities,
);
}
FOSRest manual gives the annotation for GET as
use FOS\RestBundle\Controller\Annotations\Get;
/**
* GET Route annotation.
* #Get("/likes/{type}/{typeId}")
*/
When I use the route as /index, it gives me an error and my route definition in config.yml is:
index:
type: rest
resource: Company\WebservicesBundle\Controller\UserController
How can I fix this problem?
If I were you, I would create separate bundles for your REST controllers and your generic CRUD controllers. This will make things easier to maintain (in my opinion). For example, create a AcmeMainBundle and a AcmeRestBundle, and then create a separate class to actually perform the actions that you will call from both bundles. Something like this:
// src/Acme/MainBundle/Crud/User.php (create this folder structure)
class User{
private $em;
public function __construct($em){
$this->em = $em;
}
public function getUser($id){
return $this->em->getRepository('AcmeMainBundle:User')->find($id);
}
}
Then:
// src/Acme/MainBundle/Controller/UserController.php
use Symfony\Component\HttpFoundation\Request;
use Acme\MainBundle\Crud\User;
class UserController extends Controller {
public function getAction($request){
$em = $this->getDoctrine()->getManager();
$getUser = new User($em);
$user = $getUser ->getUser($request->query->get('user_id'));
// return response
}
}
And then:
// src/Acme/RestBundle/Controller/UserController.php
use FOS\RestBundle\Controller\Annotations as Rest;
use FOS\RestBundle\Routing\ClassResourceInterface;
use Symfony\Component\HttpFoundation\Request;
class UserController extends Controller implements ClassResourceInterface {
/**
* #Rest\View()
*/
public function getAction($id){
$em = $this->getDoctrine()->getManager();
$getUser = new User($em);
$user = $getUser ->getUser($id);
// return using the default format defined in config.yml
return array(
"success"=>'true',
"user" => $user
);
} // get_user [GET] /users/{id}
}
Please note that using the ClassResourceInterface means your method names will be used to generate the routes. see FOSRestBundle Docs for more info on that.
You can do something similar to this for all your CRUD, that way you keep your routes separate and maintainable, but still have a single code base to update.
Say I have a FormType in a re-usable bundle like so:
/**
* #Service("acme_core_purchase.form.add_to_basket")
* #FormType
*/
class AddToBasketType extends AbstractType
{}
In my controller I inject it, to do whatever I need:
/** #Inject("acme_core_purchase.form.add_to_basket") */
protected $add_to_basket_form;
Say in a particular project I want to extend that form and add some extra fields in, how am I meant to do this with annotations?
class AddToBasketType extends BaseAddToBasketType
{}
I can't just create a form with the same service name, that doesn't
work / make sense- the assignment doesn't follow the priority set in
AppKernel::registerBundles as far as I can tell. I suspect this is
conceptually flawed anyway.
I can change the service name, but then I need to inject a different
service everywhere I've used it, which involves extending all of
those things.
I can store the service name in a parameter then inject that
everywhere instead, that way I can have the correct service name
injected everywhere, but I'd also need to inject the container to be
able to retrieve the service.
I've been doing the latter wherever I've needed to, but surely there
is a better way? Or at least, there should be.
Simple, use the Form component own inheritance.
/**
* #Service("acme_core_purchase.form.add_to_basket")
* #FormType
*/
class AddToBasketType extends AbstractType
{
public function getName() { return 'add_to_basket'; }
public function getParent() { return 'form'; }
}
/**
* #Service("whatever")
* #FormType
*/
class AddToBasket2Type extends AbstractType
{
public function getName() { return 'add_to_basket2'; }
public function getParent() { return 'add_to_basket'; }
}
Take a look at the service piece of their annotation configs
Notice the parent="another.service.id"
<?php
use JMS\DiExtraBundle\Annotation\Service;
/**
* #Service("some.service.id", parent="another.service.id", public=false)
*/
class Listener
{
}
I've defined a route in my app routing file:
RouteName:
pattern: /some/route
defaults: { _controller: MyAppBundle:Controller:action }
In a controller I can use:
$this->get('router')->generate('RouteName');
How would I simply access that from a fresh class I create, for example a view class that doesn't extend anything:
namespace My\AppBundle\View;
class ViewClass {
public function uri()
{
return getTheRoute('RouteName');
}
}
You need to inject "router" service into your ViewClass. Eg. in place where your define your ViewClass service:
viewclass.service:
class: Namespace\For\ViewClass
arguments:
router: "#router"
and then in your constructor:
public function __construct(\Symfony\Bundle\FrameworkBundle\Routing\Router $router)
{
$this->router = $router;
}
The clue is in how the $this->generateUrl() method works in Controllers. See:
/**
* Generates a URL from the given parameters.
*
* #param string $route The name of the route
* #param mixed $parameters An array of parameters
* #param Boolean $absolute Whether to generate an absolute URL
*
* #return string The generated URL
*/
public function generateUrl($route, $parameters = array(), $absolute = false)
{
return $this->container->get('router')->generate($route, $parameters, $absolute);
}
So you'll need to define your class as a service and inject the #router service. Either that or have your class implement ContainerAwareInterface, but the first method would definitely be better.
You should register your class as a service and insert the router as a dependency.
See the chapter on the service container in the excellent symfony2 docs.
If you're not familiar with the concepts of the service container and dependency injection, you might feel a bit overwhelmed. However, try your best to understand it because it is a essential part of the symfony2 architecture.
You could pass the entire container from your controller to your view class on instantiation. This is NOT BEST PRACTICE and not recommended.
class View
{
protected $container;
public function __construct(\Symfony\Component\DependencyInjection\Container $container)
{
$this->container = $container;
}
}
Then in your code you could use
$this->container->get('router')->generate($route, $parameters, $absolute);