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.
Related
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.
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
How should I write in log file the error or the message I want to from an Entity class?
The idea is like this: I have some items with some properties and also a configuration with some properties. I need to check if the item has the property that also exists in the configuration properties. When I debug my application, at some point I am here:
public function getProperty(idProperty $propertyId)
{
$properties = $this->ItemProperties();
if (isset($properties[$propertyId->getValue()])) {
return $properties[$propertyId->getValue()];
}else{
//here I want to write in the log file that the propertyId is not in the $properties.
}
return null;
}
So how can I achieve that? Thank you.
You can throw Exception and setup Exception Listener, which will write into log.
Inside Entity:
if (isset($properties[$propertyId->getValue()])) {
return $properties[$propertyId->getValue()];
} else {
throw new DomainException('Something is wrong.' . print_r($this, true));
}
In Listener class:
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
$e = $event->getException();
if ($e instanceof DomainException) {
$this->logger->warning('Exception ' . get_class($e) , ['message' => $e->getMessage()]);
$event->setResponse(
new JsonResponse(['error' => $e->getMessage()], 400)
);
services.yml
app.exception_listener:
class: Application\Listeners\ExceptionListener
arguments: ['#domain.logger']
tags:
- { name: kernel.event_listener, event: kernel.exception }
Symfony Events.
You can also inject Logger into your Entity and write to log from there, though it violates Single Responsibility Principle.
UPDATE
DomainException is a simple class, inheriting from \Exception. In this example it only exists to distinguish between your custom exceptions and those that are thrown by PHP or other libraries.
It can also contain additional functionality, for example, accepting two messages in constructor, writing one of them into log file and outputting another for your user.
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.
I prepare external bundle and I would like to add some routes during compilation passes.
Routes will be created on the main app/config/config.yml settings.
I was trying to get router from ContainerBuilder in my CustomCompilerPass via:
$definition = $container->getDefinition('router');
, but I got The service definition "router" does not exist.
Is it possible to add custom routes during compilation passes?
There's no way to add routes at compiler passes.
In order to dynamicly load routes (aware of container parameters) I'd use a custom route loader as given in my previous example
class MyLoader extends Loader
{
protected $params;
public function __construct($params)
{
$this->params = $params;
}
public function supports($resource, $type = null)
{
return $type === 'custom' && $this->params == 'YourLogic';
}
public function load($resource, $type = null)
{
// This method will only be called if it suits the parameters
$routes = new RouteCollection;
$resource = '#AcmeFooBundle/Resources/config/dynamic_routing.yml';
$type = 'yaml';
$routes->addCollection($this->import($resource, $type));
return $routes;
}
}
routing.yml
_custom_routes:
resource: .
type: custom
router is an alias, not a service. To get that from a ContainerBuilder, use ContainerBuilder::getAlias. To get the service ID, you need to cast that object to a string: (string) $container->getAlias('router'). Now, you can use that ID to get the service: $container->getDefinition($container->getAlias('router')). And then you get the Service which you can modify to add routes.
BTW, I'm not sure if this is really the thing you want. What about using the CmfRoutingBundle. Then, you use the Chain Router, so you can use both the Symfony2 router and the DynamicRouter. The DynamicRouter can be used with a custom route provider, in which you return the routes you want (you can get them from every resource you want).