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

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.

Related

Removing some schemas/models from API-Platforms Swagger/OpenAPI documentation output

API-Platform will generate Swagger/OpenAPI route documentation and then below documentation for the Schemas (AKA Models) (the docs show them as "Models" but current versions such as 2.7 show them as "Schemas").
Where is the content generated to show these schemas/models? How can some be removed? The functionality to display them is part of Swagger-UI, but API-Platform must be responsible for providing the JSON configuration and thus which to change API-Platform and not Swagger-UI. Note that this post shows how to add a schema but not how to remove one. Is there any documentation on the subject other than this which doesn't go into detail?
As seen by the output below, I am exposing AbstractOrganization, however, this class is extended by a couple other classes and is not meant to be exposed, but only schemas for the concrete classes should be exposed. Note that my AbstractOrganization entity class is not tagged with #ApiResource and is not shown in the Swagger/OpenAPI routing documentation but only the schema/model documentation.
Thank you
I am pretty certain there are better ways to implement this, however, the following will work and might be helpful for others.
<?php
declare(strict_types=1);
namespace App\OpenApi;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\OpenApi;
use ApiPlatform\Core\OpenApi\Model\Paths;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class OpenApiRouteHider implements OpenApiFactoryInterface {
public function __construct(private OpenApiFactoryInterface $decorated, private TokenStorageInterface $tokenStorage)
{
}
public function __invoke(array $context = []): OpenApi
{
$openApi = $this->decorated->__invoke($context);
$removedPaths = $this->getRemovedPaths();
$paths = new Paths;
$pathArray = $openApi->getPaths()->getPaths();
foreach($openApi->getPaths()->getPaths() as $path=>$pathItem) {
if(!isset($removedPaths[$path])) {
// No restrictions
$paths->addPath($path, $pathItem);
}
elseif($removedPaths[$path]!=='*') {
// Remove one or more operation
foreach($removedPaths[$path] as $operation) {
$method = 'with'.ucFirst($operation);
$pathItem = $pathItem->$method(null);
}
$paths->addPath($path, $pathItem);
}
// else don't add this route to the documentation
}
$openApiTest = $openApi->withPaths($paths);
return $openApi->withPaths($paths);
}
private function getRemovedPaths():array
{
// Use $user to determine which ones to remove.
$user = $this->tokenStorage->getToken()->getUser();
return [
'/guids'=>'*', // Remove all operations
'/guids/{guid}'=>'*', // Remove all operations
'/tenants'=>['post', 'get'], // Remove only post and get operations
'/tenants/{uuid}'=>['delete'], // Remove only delete operation
'/chart_themes'=>'*',
'/chart_themes/{id}'=>['put', 'delete', 'patch'],
];
}
}

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

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

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.

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.

flexunit: Parametrized tests

I am trying to run a parametrized tests... Was trying to implement it like it explained here:
http://docs.flexunit.org/index.php?title=Parameterized_Test_Styles
Here is what my test case looking
import org.flexunit.runners.Parameterized;
[RunWith("org.flexunit.runners.Parameterized")]
public class ArrayBasedStackTests
{
[Paremeters]
public static var stackProvider:Array = [new ArrayBasedStack(), new LinkedListBasedStack()] ;
private var _stack:IStack;
public function ArrayBasedStackTests(param:IStack)
{
_stack = param;
}
[Before]
public function setUp():void
{
}
[After]
public function tearDown():void
{
}
[Test ( description = "Checks isEmpty method of the stack. For empty stack", dataProvider="stackProvider" )]
public function isEmptyStackPositiveTest():void
{
var stack:IStack = _stack;
assertEquals( true, stack.isEmpty() );
}
But this code throws following initializing Error:
Error: Custom runner class org.flexunit.runners.Parameterized should
be linked into project and implement IRunner. Further it needs to have
a constructor which either just accepts the class, or the class and a
builder.
Need help to fix it
UPDATE
I've updated the code so it looks like this
[RunWith("org.flexunit.runners.Parameterized")]
public class ArrayBasedStackTests
{
private var foo:Parameterized;
[Parameters]
public static function stacks():Array
{
return [ [new ArrayBasedStack()], [new LinkedListBasedStack()] ] ;
}
[Before]
public function setUp():void
{
}
[After]
public function tearDown():void
{
}
[Test ( description = "Checks isEmpty method of the stack. For empty stack", dataProvider="stacks")]
public function isEmptyStackPositiveTest(stack:IStack):void
{
assertEquals( true, _stack.isEmpty() );
}
It works. But the result is a bit strange. I have 4 test executed instead of 2. (I have 2 items in data provider, so cant get why do I have 4 tests).
Output
http://screencast.com/t/G8DHbcjDUkJ
The [Parameters] meta-data specifies that the parameters are passed to the constructor of the test - so the test class is called for each parameter. You also have the dataProvider set for the specific test method, so the test method is also called once for each parameter. Two calls for the test, and two calls to the method, ends up running four tests.
The solution is to either use [Parameters] meta-tag which specifies the data to use for the whole test class, or use the dataProvider for each test method, but not both with the same data at the same time.
You're missing the static reference to Paramaterized, as shown here:
import org.flexunit.runners.Parameterized;
[RunWith("org.flexunit.runners.Parameterized")]
public class MyTestNGTest
{
private var foo:Parameterized;
...
Basically, that error means that the [Runner] defined isn't available at runtime, which occurs if there is no static reference in the class to cause it to get linked in.
In FlexUnit 4.5.1, this approach changed to using [Rule]'s like so:
public class MyTestNGTest
{
[Rule]
public function paramaterizedRule:ParamaterizedRule = new ParamaterizedRule();
...
}
However, I can't seem to see an actual implementation of IMethodRule for paramaterized tests (that example is fictional).

Resources