how to use Symfony methods Action excluding the "Action" word - symfony

I am currently migrating an existent application to Symfony2 that has about 100 controllers with approximately 8 actions in each controller. All the current Actions are named as follow:
public function index(){}
However the default naming convention for Symfony is indexAction().
Is it possible to keep all my current actions and tell Symfony to use as it is without the "Action" word after the method name?
thank you.

Yes, this is possible. You should be able to define routes as normal, but you need to change the way the kernel finds the controller. The best way to do this is to replace/decorate/extends the service 'controller_name_converter'. This is a private service and is injected into the 'controller_resolver' service.
The source code of the class you want to replace is at 'Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser'.
Basically, the code runs like this. The 'bundle:controller:action' you specified when creating the route is saved in the cache. When a route is matched, that string is given back to the kernel, which in turn calls 'controller_resolver' which calls 'controller_name_resolver'. This class convert the string into a "namespace::method" notation.
Take a look at decorating services to get an idea of how to do it.
Here is an untested class you can work with
class ActionlessNameParser
{
protected $parser;
public function __construct(ControllerNameParser $parser)
{
$this->parser = $parser;
}
public function parse($controller)
{
if (3 === count($parts = explode(':', $controller))) {
list($bundle, $controller, $action) = $parts;
$controller = str_replace('/', '\\', $controller);
try {
// this throws an exception if there is no such bundle
$allBundles = $this->kernel->getBundle($bundle, false);
} catch (\InvalidArgumentException $e) {
return $this->parser->parse($controller);
}
foreach ($allBundles as $b) {
$try = $b->getNamespace().'\\Controller\\'.$controller.'Controller';
if (class_exists($try)) {
// You can also try testing if the action method exists.
return $try.'::'.$action;
}
}
}
return $this->parser->parse($controller);
}
public function build($controller)
{
return $this->parser->build($controller);
}
}
And replace the original service like:
actionless_name_parser:
public: false
class: My\Namespace\ActionlessNameParser
decorates: controller_name_converter
arguments: ["#actionless_name_parser.inner"]

Apparently the Action suffix is here to distinguish between internal methods and methods that are mapped to routes. (According to this question).
The best way to know for sure is to try.
// src/AppBundle/Controller/HelloController.php
namespace AppBundle\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class HelloController
{
/**
* #Route("/hello/{name}", name="hello")
*/
public function indexAction($name)
{
return new Response('<html><body>Hello '.$name.'!</body></html>');
}
}
Try to remove the Action from the method name and see what happens.

Related

Symfony - configure class from `service.yaml` with static default value

I am trying to create a Class that can be call from anywhere in the code.
It accepts different parameters that can be configured from the constructor (or setters).
This Class will be shared between several projects, so I need to be able to easily configure it once and use the same configuration (or different/specific one) multiple times.
Here's my class:
namespace Allsoftware\SymfonyBundle\Utils;
class GdImageConverter
{
public function __construct(
?int $width = null,
?int $height = null,
int|array|null $dpi = null,
int $quality = 100,
string $resizeMode = 'contain',
) {
$this->width = $width ? \max(1, $width) : null;
$this->height = $height ? \max(1, $height) : null;
$this->dpi = $dpi ? \is_int($dpi) ? [\max(1, $dpi), \max(1, $dpi)] : $dpi : null;
$this->quality = \max(-1, \min(100, $quality));
$this->resizeMode = $resizeMode;
}
}
Most of the time, the constructor parameters will be the same for ONE application.
So I thought of using a private static variable that corresponds to itself, but already configured.
So I added the $default variable:
namespace Allsoftware\SymfonyBundle\Utils;
class GdImageConverter
{
private static GdImageConverter $default;
public function __construct(
?int $width = null,
?int $height = null,
int|array|null $dpi = null,
int $quality = 100,
string $resizeMode = 'contain',
) {
// ...
}
public static function setDefault(self $default): void
{
self::$default = $default;
}
public static function getDefault(): self
{
return self::$default ?? self::$default = new self();
}
}
Looks like a Singleton but not really.
To set it up once and use GdImageConverter::getDefault() to get it, I wrote these lines inside the service.yaml file:
services:
default.gd_image_converter:
class: Allsoftware\SymfonyBundle\Utils\GdImageConverter
arguments:
$width: 2000
$height: 2000
$dpi: 72
$quality: 80
$resizeMode: contain
Allsoftware\SymfonyBundle\Utils\GdImageConverter:
calls:
- setDefault: [ '#default.gd_image_converter' ]
ATE when calling GdImageConverter::getDefault(), it does not correspond to the default.gd_image_converter service.
$default = GdImageConverter::getDefault();
$imageConverter = new GdImageConverter(2000, 2000, 72, 80);
dump($default);
dump($imageConverter);
die();
And when debugging self::$default inside getDefault(), it's empty.
What am I doing wrong ?
Note: When I change the calls method setDefault to a non-existing method setDefaults, symfony tells me that the method is not defined.
Invalid service "Allsoftware\SymfonyBundle\Utils\GdImageConverter": method "setDefaults()" does not exist.
Thank you!
Decided to post a new and hopefully more coherent answer.
The basic problem is that GdImageConverter::getDefault(); returns an instance for which all the arguments are null. And that is because the Symfony container only creates services when they are asked for (aka injected). setDefault is never called so new self() is used.
There is a Symfony class called MimeTypes which employs a similar pattern but it does not try to customize the service so it does not matter.
There is a second problem with the way the GdImageConverter service is configured. It will basically inject a 'null' version even though it does set the default instant correctly.
To fix the second problem you need to call setDefault with the current service and just get rid of default.gd_image_converter unless you need it for something else:
services:
App\Service\GdImageConverter:
class: App\Service\GdImageConverter
public: true
arguments:
$width: 2000
$height: 2000
$dpi: 72
$quality: 80
$resizeMode: contain
calls:
- setDefault: [ '#App\Service\GdImageConverter' ]
As a side note, the static method setDefault will be called dynamically. This is a bit unusual but it is legal in PHP and Symfony does it for other classes.
Next we need to ensure the service is always instantiated. This is a rare requirement and I don't think there is a default way to do so. But using Kernel::boot works:
# src/Kernel.php
class Kernel extends BaseKernel
{
use MicroKernelTrait;
public function boot()
{
parent::boot();
$this->container->get(GdImageConverter::class);
}
}
This ensures that the default service is set for both commands and web applications. GdImageConverter::getDefault(); can now be called at anytime and will return the initialized service. Notice that the service had to be declared public for Container::get to work.
You could stop here but always creating a service even though you probably don't usually need it is kind of annoying. It is possible to avoid doing that by injecting the container itself into your class.
This definitely violates Symfony's recommended practices and if the reader feels they need to downvote the answer for even suggesting it then do what you need to do. However the Laravel framework uses this approach (called facades) on a routine basis and those apps somehow manage to work.
use Psr\Container\ContainerInterface;
class GdImageConverter
{
private static GdImageConverter $default;
private static ContainerInterface $container; // Add this
public static function setContainer(ContainerInterface $container)
{
self::$container = $container;
}
public static function getDefault(): self
{
//return self::$default ?? self::$default = new self();
return self::$default ?? self::$default = self::$container->get(GdImageConverter::class);
}
}
# Kernel.php
public function boot()
{
parent::boot();
GdImageConverter::setContainer($this->container);
}
And now we are back to lazy instantiation.
And while I won't provide the details you could eliminate the need to inject the container as well as making the service public by injecting a GdImageConverterServiceLocater.

Decorate all services that implement the same interface by default?

I have a growing number of service classes that share a common interface (let's say BarService and BazService, that implement FooInterface).
All of these need to be decorated with the same decorator. Reading the docs, I know that I can do:
services:
App\BarDecorator:
# overrides the App\BarService service
decorates: App\BarService
Since I have to use the same decorator for different services I guess I would need to do:
services:
bar_service_decorator:
class: App\BarDecorator
# overrides the App\BarService service
decorates: App\BarService
baz_service_decorator:
class: App\BarDecorator
# overrides the App\BazService service
decorates: App\BazService
Problem is: this gets repetitive, quickly. And every time a new implementation of FooInterface is created, another set needs to be added to the configuration.
How can I declare that I want to decorate all services that implement FooInterface automatically, without having to declare each one individually?
A compiler pass allows to modify the container programmatically, to alter service definitions or add new ones.
First you'll need a way to locate all implementations of FooInterface. You can do this with the help of autoconfigure:
services:
_instanceof:
App\FooInterface:
tags: ['app.bar_decorated']
Then you'll need to create the compiler pass that collects all FooServices and creates a new decorated definition:
// src/DependencyInjection/Compiler/FooInterfaceDecoratorPass.php
namespace App\DependencyInjection\Compiler;
use App\BarDecorator;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class FooInterfaceDecoratorPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has(BarDecorator::class)) {
// If the decorator isn't registered in the container you could register it here
return;
}
$taggedServices = $container->findTaggedServiceIds('app.bar_decorated');
foreach ($taggedServices as $id => $tags) {
// skip the decorator, we do it's not self-decorated
if ($id === BarDecorator::class) {
continue;
}
$decoratedServiceId = $this->generateAliasName($id);
// Add the new decorated service.
$container->register($decoratedServiceId, BarDecorator::class)
->setDecoratedService($id)
->setPublic(true)
->setAutowired(true);
}
}
/**
* Generate a snake_case service name from the service class name
*/
private function generateAliasName($serviceName)
{
if (false !== strpos($serviceName, '\\')) {
$parts = explode('\\', $serviceName);
$className = end($parts);
$alias = strtolower(preg_replace('/[A-Z]/', '_\\0', lcfirst($className)));
} else {
$alias = $serviceName;
}
return $alias . '_decorator';
}
}
Finally, register the compiler pass in the kernel:
// src/Kernel.php
use App\DependencyInjection\Compiler\FooInterfaceDecoratorPass;
class Kernel extends BaseKernel
{
// ...
protected function build(ContainerBuilder $container)
{
$container->addCompilerPass(new FooInterfaceDecoratorPass());
}
}
Interesting! I think that's going to be tricky... but maybe with some hints here you might come up with a solution that fits your needs
find all Decorators... not sure if there's an easier way in that case but I use tags for that. So create a DecoratorInterface add auto tag it...
loop through the definitions and and modify and set the decorated service
e. g. in your Kernel or AcmeAwesomeBundle do
protected function build(ContainerBuilder $container)
{
$container->registerForAutoconfiguration(DecoratorInterface::class)
->addTag('my.decorator.tag');
$decoratorIds = $container->findTaggedServiceIds('my.decorator.tag');
foreach ($decoratorIds as $decoratorId) {
$definition = $container->getDefinition($decoratorId);
$decoratedServiceId = $this->getDecoratedServiceId($definition);
$definition->setDecoratedService($decoratedServiceId);
}
}
private function getDecoratedServiceId(Definition $decoratorDefinition): string
{
// todo
// maybe u can use the arguments here
// e.g. the first arg is always the decoratedService
// might not work because the arguments are not resolved yet?
$arg1 = $decoratorDefinition->getArgument(0);
// or use a static function in your DecoratorInterface like
// public static function getDecoratedServiceId():string;
$class = $decoratorDefinition->getClass();
$decoratedServiceId = $class::getDecoratedServiceId();
return 'myDecoratedServiceId';
}
I'm pretty sure this is not complete yet but let us know how you solved it

PHP/Symfony - Parsing object properties from Request

We're building a REST API in Symfony and in many Controllers we're repeating the same code for parsing and settings properties of objects/entities such as this:
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
I'm aware that Symfony forms provide this functionality, however, we've decided in the company that we want to move away from Symfony forms and want to use something simplier and more customisable instead.
Could anybody please provide any ideas or examples of libraries that might achieve property parsing and settings to an object/entity? Thank you!
It seems like a good use case for ParamConverter. Basically it allows you, by using #ParamConverter annotation to convert params which are coming into your controller into anything you want, so you might just create ParamConverter with code which is repeated in many controllers and have it in one place. Then, when using ParamConverter your controller will receive your entity/object as a parameter.
class ExampleParamConverter implements ParamConverterInterface
{
public function apply(Request $request, ParamConverter $configuration)
{
//put any code you want here
$title = $request->request->get('title');
if (isset($title)) {
$titleObj = $solution->getTitle();
$titleObj->setTranslation($language, $title);
$solution->setTitle($titleObj);
}
//now you are setting object which will be injected into controller action
$request->attributes->set($configuration->getName(), $solution);
return true;
}
public function supports(ParamConverter $configuration)
{
return true;
}
}
And in controller:
/**
* #ParamConverter("exampleParamConverter", converter="your_converter")
*/
public function action(Entity $entity)
{
//you have your object available
}

Facade pattern for Symfony2 services

New to Symfony2, I'm building an app that uses an external API to get data. I created a lot of client classes to retrieve and transform each entity from the API, and I defined those classes as services - e.g., I have a FooClient with methods like getAll() or getThoseThatInterestMe($me), which return data from the API.
Now I wanted to create a ApiClientFacade class, which acts as an interface in front of all the XxxClient classes, following the Facade Pattern - e.g., this facade class would have a method getAllFoo(), which in turn would call FooClient::getAll(), and so on...
I could define my facade class as a service as well, but it'd have too many dependencies - I have around 30 client classes. Also, afaik with this approach I'd be loading all 30 dependencies every time, while most of the times I'd only need one dependency...
So, is there a better way to do this?
Use additional ApiClientFactory to move responsibility about "instantiation of ApiClient objects" from your ApiFacade class (which is your initial idea, as I understood).
In some pseudo-php code my idea is:
$api = new ApiFacade(new ApiClientFactory);
$api->sendNotificationAboutUserLogin('username', time());
An example of method:
class ApiFacade {
private $apiFactory;
public function __construct(ApiClientFactory $factory)
{
$this->apiFactory = $factory;
}
public function sendNotificationAboutUserLogin($username, $timeOfOperation)
{
return $this->apiFactory
->createApi('User')
->post(
'notifications',
array('operation' => 'login', 'username' => $username, 'timeOfOperation' => $timeOfOperation)
);
}
}
In this case your Facade class stays injectable (testable), but also becomes simpler instantiatable (you don't need to pass all dependencies into it anymore).
The ApiClientFactory should look like that:
class ApiClientFactory {
private $apiBaseUrl;
public function __construct($apiBaseUrl)
{
$this->apiBaseUrl = $apiBaseUrl;
}
public function createApi($apiName)
{
switch ($apiName) {
case 'User': return new \My\UserApi($this->apiBaseUrl);
default: // throw an exception?
}
}
}

Symfony Custom Route loader, loading multiple times, getting exception

I'm trying to make custom routeloader according to http://symfony.com/doc/current/cookbook/routing/custom_route_loader.html
my code looks like this
//the routeloader:
//the namespace and use code ....
class FooLoader extends Loader{
private $loaded = false;
private $service;
public function __construct($service){
$this->service = $service;
}
public function load($resource, $type=null){
if (true === $this->loaded)
throw new \RuntimeException('xmlRouteLoader is already loaded');
//process some routes and make $routeCollection
$this->loaded = true;
return $routeCollection;
}
public function getResolver()
{
// needed, but can be blank, unless you want to load other resources
// and if you do, using the Loader base class is easier (see below)
}
public function setResolver(LoaderResolverInterface $resolver)
{
// same as above
}
function supports($resource, $type = null){
return $type === 'xmlmenu';
}
}
//the service definition
foo.xml_router:
class: "%route_loader.class%"
arguments: [#foo.bar_service] //this service and the injection has been tested and works.
tags:
- { name: routing.loader }
//the routing definitions
//routing_dev.yml
_foo:
resource: "#FooBarBundle/Resources/config/routing.yml"
-----------------------------
//FooBarBundle/Resources/config/routing.yml
_xml_routes:
resource: .
type: xmlmenu
and when I try to access any route I get the exception:
RuntimeException: xmlRouteLoader is already loaded
which is the exception I defined if the loader is loaded multiple times.So why does it try to load this loader more than once? and I'm pretty sure I've defined it only there.
Actually the answer was quite simple.it seems like this method only supports one level of imports.I only needed to put the _xml_routes directly under routing_dev.yml, otherwise it somehow winds out in a loop.explanations to why that is are appreciated.

Resources