Inject service based on dynamic value in Symfony - symfony

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);
}

Related

Phpunit reusable test methods in symfony

Let's assume that I have three entites: UserEntity, ZooEntity and AnimalEntity. In order to create zoo I have to create user, because zoo has to have owner. In order to create animal I have to create zoo.
Going further I have 3 controllers for each entity. I also have 3 test classes for controller testing.
UserControllerTest
ZooControllerTest
AnimalControllerTest
In animal test, every time, in each test (to make every test independent) I have to create user and then zoo. Therefore I created traits eg: UserTestTrait and ZooTestTrait which have createUser and createZoo(user) methods.
I was wondering about chaning those traits into services. But then where I should keep them?
Would be tests/services/ZooService a good place?
For now I such structure:
tests/Controller/Rest/ZooControllerTest
tests/Controller/Rest/traits/ZooTestTrait
Assuming that I have those services and every service should have access to eg. entity manager, how can I access that in service? eg. ZooService located in tests/services/ZooService
How can I use that service in controller? Lets assume that I would like to have it in setUp method:
protected function setUp(): void {
$kernel = self::bootKernel();
// access the zoo service, that has access to the entity manager
}
I found myself a better approach. Using DoctrineFixturesBundle is a very satisfying way of providing test data. It's even better, when you integrate the Fixtures into your Tests - which might slow them down, but the quality gain is super convenient.
see this tutorial
My AbstractControllerTest Class looks similar to this:
<?php
namespace App\Tests\Functional\Controller;
use App\DataFixtures\AbstractAppFixtures;
use App\DataFixtures\UserFixtures;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader;
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\Cookie;
abstract class AbstractControllerTest extends WebTestCase
{
/**
* #var ORMExecutor
*/
private $fixtureExecutor;
/**
* #var ContainerAwareLoader
*/
private $fixtureLoader;
/**
* set up before test
*/
public function setUp(): void {
$kernel = static::getKernelClass();
static::$kernel = new $kernel('dev', true);
static::$kernel->boot();
static::$container = static::$kernel->getContainer();
$this->addFixture(new UserFixtures());
}
/**
* #param AbstractAppFixtures $fixture
*/
protected function addFixture(AbstractAppFixtures $fixture) {
$add = true;
$activeFixtures = $this->getFixtureLoader()->getFixtures();
foreach ($activeFixtures as $activeFixture) {
if (get_class($activeFixture) === get_class($fixture)) {
$add = false;
}
}
if ($add) {
$this->getFixtureLoader()->addFixture($fixture);
if ($fixture instanceof DependentFixtureInterface) {
/** #var AbstractAppFixtures $parentFixture */
foreach ($fixture->getDependencies() as $parentFixture) {
if (class_exists($parentFixture)) {
$this->addFixture(new $parentFixture());
}
}
}
}
}
/**
*
*/
protected function executeFixtures() {
$this->getFixtureExecutor()->execute($this->getFixtureLoader()->getFixtures());
}
protected function getContainer() {
if (static::$container) {
return static::$container;
}
return static::$kernel->getContainer();
}
/**
* #return ORMExecutor
*/
private function getFixtureExecutor() {
if (!$this->fixtureExecutor) {
$em = $this->getEm();
$this->fixtureExecutor = new ORMExecutor($em, new ORMPurger($em));
}
return $this->fixtureExecutor;
}
/**
* #return ContainerAwareLoader
*/
private function getFixtureLoader() {
if (!$this->fixtureLoader) {
$this->fixtureLoader = new ContainerAwareLoader($this->getContainer());
}
return $this->fixtureLoader;
}
}
One thing you might want to consider is to introduce test data builder. Just because you have that dependency of Animal -> Zoo -> Owner doesn't mean that you have to deal with it in your tests. Of course, without having seen your tests, it's hard to tell. But I assume that, when testing the AnimalController, it is important that a valid Animal exists, not so much in which exact zoo with which exact owner. But even if that's the case, you could make your test data builders do the work for you.
The data builders don't deal with persistence, though. But this is a good thing, since you could use them for both, unit and functional tests.
I usually split up my test folder usually into Unit, Integration, Functional and Support. Support being support code for the tests. Regarding your question where to put the service, this would also be in Support. That being said, you probably won't even need the service you have thought of. The data builder create the entities you need in your tests, so you just have to persist it.

Symfony FOSUserBundle error

So, I've made an app with its own controller and entities. I've just installed the FOSUserBundle following the guide and I want to connect my app to the login, register, etc. I tried using the function 'path'. This is what looks like... 'href="{{path(menu.url)}}' ok, so I get an error: 'such route does not exist'. I decided to create another controller, this one called FosController, next to my previous one the DefaultController, I extended the FosController from SecurityController(login fosuserbundle) and decided to create with annotation a route called login and connect it to the parent method...
class FosController extends SecurityController
{
/**
* #Route("/hospitallogin", name="login")
* #Method({"GET", "POST"})
*
* #param Request $request
*
* #return Response
*/
public function login2Action(Request $request) {
parent::loginAction($request);
}
}
And I get this error:
The controller must return a response (null given). Did you forget to add a return statement somewhere in your controller?
I dont know what else to do, thanks for your time.
I decided to use this though it's not correct...
public function indexAction()
{
return $this->redirect('http://localhost:8000/login');
}

Symfony validation callback

I'm trying to validate my entity via static callback.
I was able to make it work following the Symfony guide but something isn't clear to me.
public static function validate($object, ExecutionContextInterface $context, $payload)
{
// somehow you have an array of "fake names"
$fakeNames = array(/* ... */);
// check if the name is actually a fake name
if (in_array($object->getFirstName(), $fakeNames)) {
$context->buildViolation('This name sounds totally fake!')
->atPath('firstName')
->addViolation()
;
}
}
It works fine when I populate my $fakeNames array but what if I want to make it "dynamic"? Let's say I want to pick that array from the parameters or from the database or wherever.
How am I supposed to pass stuff (eg. the container or entityManager) to this class from the moment that the constructor doesn't work and it has to be necessarily static?
Of course my approach may be completely wrong but I'm just using the symfony example and few other similar issues found on the internet that I'm trying to adapt to my case.
You can create a Constraint and Validator and register it as service so you can inject entityManager or anything you need, you can read more here:
https://symfony.com/doc/2.8/validation/custom_constraint.html
or if you are on symfony 3.3 it is already a service and you can just typehint it in your constructor:
https://symfony.com/doc/current/validation/custom_constraint.html
This is the solution I was able to find in the end.
It works smoothly and I hope it may be useful for someone else.
I've set the constraint on my validation.yml
User\UserBundle\Entity\Group:
constraints:
- User\UserBundle\Validator\Constraints\Roles\RolesConstraint: ~
Here is my RolesConstraint class
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\Validator\Constraint;
class RolesConstraint extends Constraint
{
/** #var string $message */
public $message = 'The role "{{ role }}" is not recognised.';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
and here is my RolesConstraintValidator class
<?php
namespace User\UserBundle\Validator\Constraints\Roles;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class RolesConstraintValidator extends ConstraintValidator
{
/** #var ContainerInterface */
private $containerInterface;
/**
* #param ContainerInterface $containerInterface
*/
public function __construct(ContainerInterface $containerInterface)
{
$this->containerInterface = $containerInterface;
}
/**
* #param \User\UserBundle\Entity\Group $object
* #param Constraint $constraint
*/
public function validate($object, Constraint $constraint)
{
if (!in_array($object->getRole(), $this->containerInterface->getParameter('roles'))) {
$this->context
->buildViolation($constraint->message)
->setParameter('{{ role }}', $object->getRole())
->addViolation();
}
}
}
Essentially, I set up a constraint which, every time a new user user is registered along with the role, that role must be among those set in the parameters. If not, it builds a violation.

How to get #request_stack service in app/console context?

I have services that require the #request_stack to fetch parameters.
Now, I want to expose certain functionality to console commands callable via ./app/console//. Yet in the context of an ./app/console, there is no #request_stack, yet one can input arguments.
In order to resolve this issue, I am now creating basically two services, one basic, only waiting for the params, and one being able to use the #request_stack.
Yet I dislike that there are two ways for the data to be fetched in the request-based flow and via the app/console.
Hence I am wondering, as I am simply want the data that comes per default via the request to also be able to be inputted via console arguments:
Can I setup a custom request_stack to simulate a request during a console command?
When I was investigating this issue, I stumbled across request stack push method, where a warning was already in place in the doc block:
/**
* Pushes a Request on the stack.
*
* This method should generally not be called directly as the stack
* management should be taken care of by the application itself.
*/
public function push(Request $request)
{
$this->requests[] = $request;
}
So while it would be possible to do it this way, I decided against the approach of my original question and to refactor my application instead.
I have created a context value object which just holds the parameter data:
/**
* Context
**/
class Context
{
/**
* #var string
*/
private $countryCode;
/**
* Context constructor.
* #param string $countryCode
*/
public function __construct($countryCode = '')
{
$this->countryCode = $countryCode;
}
/**
* #return string
*/
public function getCountryCode()
{
return $this->countryCode;
}
}
And a ContextFactory that creates the context with by the request stack:
class ContextFactory extends RequestAwareService
{
/**
* ContextFactory constructor.
* #param RequestStack $stack
*/
public function __construct(RequestStack $stack)
{
$this->setRequestStack($stack);
}
/**
* #return Context
*/
public function create()
{
return new Context($this->request->getCountryCode());
}
}
(The RequestAwareService is just a helper class to more easily parse the request.)
I then defined the services in my Bundle services.yml:
context.factory:
class: Kopernikuis\MyBundle\Service\Config\ContextFactory
arguments:
- '#request_stack'
context:
class: Kopernikuis\MyBundle\Service\Config\Context
factory:
- '#context.factory'
- create
Instead of injecting the #request_stack, I am now injecting my #context value object, which also had the benefit of reducing the hierarchy as now only one service parses the request_stack once, and I also noticed that certain functionality got much simpler as I could remove parameters from method calls, as they were all provided by the context object instead.
And in my custom commands, I can just replace my context
protected function execute(InputInterface $input, OutputInterface $output)
{
// #todo: use variable from InputInterface
$context = new Context('fnordfoo');
$this->getContainer()->set('context', $context);
}
With the newly gained knowledge, I strongly disagree with my original intent of trying to manually set the #request_stack.
Refactoring the code base to not necessarily require the #request_stack was a more solid choice.

Symfony services --> correct usage?

first of all I am trying to use services for the first time... (Actually if s.o. could give a short info about how and when and why to use it.. nice ;-) )
But now to my specific case:
I wrote two controllers:
One for uploading a xlsx file to the server
One for importing the xlsx data to the DB
What I now want to do is to pass the (uploaded)path from the uploading controller to the import controller. am I correct to use the import as a service?
Code looks as the following...
class FileUploadController extends Controller
/**
* #Route("/upload", name="upload")
* #Security("has_role('ROLE_ADMIN')")
*/
public function uploadAction(Request $request){
$companyid = $this->getUser()->getCompany();
if ($request->getMethod() == 'POST'){
$file = $request->files->get('xls');
$uploadedURL = '';
if(($file instanceof UploadedFile) && $file->getError()=='0'){
if(!($file->getSize()<20000)){
$originalName = $file->getClientOriginalName();
$name_array = explode('.',$originalName );
$file_type = $name_array[(sizeof($name_array)-1)];
$valid_filetypes = array('xls', 'xlsx');
if(in_array(strtolower($file_type), $valid_filetypes)){
$document = new Document();
$document->setFile($file);
$document->setSubDirectory('uploads');
$document->processFile();
$uploadedURL=$uploadedURL=$document->getUploadDirectory().DIRECTORY_SEPARATOR.$document->getSubDirectory().DIRECTORY_SEPARATOR.$file->getBasename();
}else{
echo "Wrong File Ending";
}
}else {
echo "File to big";
}
}else{
print_r('File Error');
die;;
}
$this->get("dataimport.service")->importIndexAction($uploadedURL);
}else{
return $this->render(bla)
DataImportController as:
class DataImportController extends Controller
/**
* #param ContainerInterface $container
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
/**
* #Security("has_role('ROLE_ADMIN')")
*/
public function importIndexAction($path)
{
$companyid = $this->getUser()->getCompany();
$em = $this->getDoctrine()->getManager();
$file = $this->defineFilePathAction($path);
$reader = $this->readExcelAction($file);
$accountarray = $this->getAccountsArrayAction($companyid);
$this->importAccountsAction($companyid, $reader, $accountarray, $em);
}
....
/**
* Get a service from the container
*
* #param string The service to get
*/
public function get($service)
{
return $this->container->get($service);
}
services.yml
services:
dataimport.service:
class: AppBundle\Controller\DataHandling\DataImportController
arguments: [#service_container]
Thanks for your help!
In Symfony a controller and service is at first just a class. A controller's public method is meant to take an input and generates an Response (output) (by the way injecting the Request is deprecated, you have to use the current request from the request_stack). A service is an object out of the DI container with no constraints at all.
Since the controller's method has to generate and return a response, it's mostly not a good idea to invoke a controller from another controller, because you maybe don't need that response, but only the implementation of the method.
That's also the reason why you should move reusable code to services. A controller should actually only:
extract data from the request
call some services
render a template with the result
Same for Commands. The services are the core of your application. Horizontal communication between controllers or commands is mostly a bad idea (only of course some proxies or wrapper).
Here are some ideas for your code:
The action itself is too much unreadable code. If you get the uploaded file via symfony form read this https://stackoverflow.com/a/28754907/4469738
Don't access the request directly if you use forms. The reason is, that only your builder or Type class which creates the form (and the data class), knows the name of the input fields and maps them to a data class. You should just use the data class. Then you get a nice UploadedFile object to check everything, but also move the checks to services.

Resources