How to provide Symfony routing parameter programatically? - php

In this Symfony route
/**
* #Route("/board/{board}/card/{card}", name="card_show", methods={"GET"}, options={})
*/
public function show(Board $board, Card $card): Response
{
$card->getLane()->getBoard(); // Board instance
// ...
}
How is it possible to add the {board} parameter programatically, since it is already available in {card}? Now, I always need to add two parameters, when generating links to show action.
After some research I've found the RoutingAutoBundle (https://symfony.com/doc/master/cmf/bundles/routing_auto/introduction.html#usage) which would provide the functions I need, but it's not available for Symfony 5 anymore.
Thanks.

Okay, after some investigation I've found this question
Which lead me to this helpful answer.
My controller action (with #Route annotation) looks like this:
/**
* #Route("/board/{board}/card/{card}", name="card_show", methods={"GET"})
*/
public function show(Card $card): Response
{
}
We just have one argument ($card) in method signature, but two arguments in route.
This is how to call the route in twig:
path("card_show", {card: card.id})
No board parameter required, thanks to a custom router.
This is how the custom router looks like:
<?php // src/Routing/CustomCardRouter.php
namespace App\Routing;
use App\Repository\CardRepository;
use Symfony\Component\Routing\RouterInterface;
class CustomCardRouter implements RouterInterface
{
private $router;
private $cardRepository;
public function __construct(RouterInterface $router, CardRepository $cardRepository)
{
$this->router = $router;
$this->cardRepository = $cardRepository;
}
public function generate($name, $parameters = [], $referenceType = self::ABSOLUTE_PATH)
{
if ($name === 'card_show') {
$card = $this->cardRepository->findOneBy(['id' => $parameters['card']]);
if ($card) {
$parameters['board'] = $card->getLane()->getBoard()->getId();
}
}
return $this->router->generate($name, $parameters, $referenceType);
}
public function setContext(\Symfony\Component\Routing\RequestContext $context)
{
$this->router->setContext($context);
}
public function getContext()
{
return $this->router->getContext();
}
public function getRouteCollection()
{
return $this->router->getRouteCollection();
}
public function match($pathinfo)
{
return $this->router->match($pathinfo);
}
}
Now, the missing parameter board is provided programatically, by injecting and using the card repository. To enable the custom router, you need to register it in your services.yaml:
App\Routing\CustomCardRouter:
decorates: 'router'
arguments: ['#App\Routing\CustomCardRouter.inner']

Related

Laravel route resolve custom data type

I have the following routes in routes/api.php:
Route::get('items/{item}', function(Guid $item) {...});
Route::get('users/{user}', function(Guid $user) {...});
Since Guid is a custom type, how can I resolve that via dependency injection? As shown, the route parameter {item} differs from the callback parameter type-hint:Guid so it can not be automatically resolved.
That's what I've tried in app/Providers/AppServiceProvider.php:
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* #return void
*/
public function register()
{
$this->app->bind(Guid::class, function(Application $app, array $params) {
return Guid::fromString($params[0]);
});
}
}
I'd expect $params to be something like this: [ 'item' => 'guid' ] -- but it is: [].
You can make use of explicit binding Laravel Routing:
in RouteServiceProvider::boot():
public function boot()
{
Route::model('item', Guid $item);
Route::model('user', Guid $user);
}
If Guid is not a model use a Closure to map onto the string:
Route::bind('user', function ($value) {
return Guid::fromString($value);
});
UPDATED
And I found another way, much better - implement UrlRoutable contract Lavaravel API:
<?php
namespace App\Models;
use Illuminate\Contracts\Routing\UrlRoutable;
class Guid implements UrlRoutable
{
private string $guid;
public function setGuid(string $guid)
{
$this->guid = $guid;
return $this;
}
public function getGuid(): string
{
return $this->guid;
}
public static function fromString(string $guid): self
{
//you cannot set props from constructor in this case
//because binder make new object of this class
//or you can resolve constructor depts with "give" construction in ServiceProvider
return (new self)->setGuid($guid);
}
public function getRouteKey()
{
return $this->guid;
}
public function getRouteKeyName()
{
return 'guid';
}
public function resolveRouteBinding($value, $field = null)
{
//for using another "fields" check documentation
//and maybe another resolving logic
return self::fromString($value);
}
public function resolveChildRouteBinding($childType, $value, $field)
{
//or maybe you have relations
return null;
}
}
And, with this, you can use routes like you want as Guid now implements UrlRoutable and can turn {item} (or whatever) URL-path sub-string markers into Guids per dependency injection (by the type-hint as you asked for it):
Route::get('items/{item}', function(Guid $item) {
return $item->getGuid();
});
BTW: NEVER EVER use closures in routes as you cannot cache closure routes - and routes are good to be optimized, and caching helps with that in Laravel routing.
simple helper to utilize route binding callback.
if (!function_exists('resolve_bind')) {
function resolve_bind(string $key, mixed $value) {
return call_user_func(Route::getBindingCallback($key), $value);
}
}
usage
resolve_bind('key', 'value');

Call createForm() and generateUrl() from service in Symfony2

I would like have access to controller methods from my custom service. I created class MyManager and I need to call inside it createForm() and generateUrl() functions. In controller I can use: $this->createForm(...) and $this->generateUrl(...), but what with service? It is possible? I really need this methods! What arguments I should use?
If you look to those two methods in Symfony\Bundle\FrameworkBundle\Controller\Controller class, you will see services name and how to use them.
public function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
return $this->container->get('router')->generate($route, $parameters, $referenceType);
}
public function createForm($type, $data = null, array $options = array())
{
return $this->container->get('form.factory')->create($type, $data, $options);
}
Basically, you class need services router and form.factory for implementing functionality. I do not recommend passing controller to your class. Controllers are special classes that are used mainly by framework itself. If you plan to use your class as service, just create it.
services:
my_manager:
class: Something\MyManager
arguments: [#router, #form.factory]
Create a constructor with two arguments for services and implement required methods in your class.
class MyManager
{
private $router;
private $formFactory;
public function __construct($router, $formFactory)
{
$this->router = $router;
$this->formFactory = $formFactory;
}
// example method - same as in controller
public function createForm($type, $data = null, array $options = array())
{
return $this->formFactory->create($type, $data, $options);
}
// the rest of you class ...
}
assuming you are injecting the service into your controller , you can pass the controller object to your service function
example
class myService
{
public function doSomthing($controller,$otherArgs)
{
$controller->generateForm();
}
}
class Mycontroller extends Controller
{
public function indexAction()
{
$this->get("my-service")->doSomthing($this,"hello");
}
}

Zend Framework 2: Pass Variable to View Helper

I have created a View Helper to display latest Adverts from a Database Table. Since I have different Types of Adverts, I would like to be able to pass a variable from inside my View where I call the View Helper to show specific Adverts.
I am sorry that I can not explain it in a better way, but I am still a total beginner in ZF2. I will add the Sourcecode and hopefully this will make it more clear. Please note that I have the Sourcecode from a Book which displayed Pizza's randomly and changed it till it worked to show my adverts. I might still have Code in it which is not actually needed, so please do not wonder... Okay here the code:
1. the view: index.html
<?php foreach ($this->latestAdvert() as $value){ ?>
<li><?php echo $value->getAdvertTitle();?></li>
<?php }?>
2. the view Helper: Advert\View\Helper\LatestAdvert.php
namespace Advert\View\Helper;
use Zend\View\Helper\AbstractHelper;
class LatestAdvert extends AbstractHelper
{
protected $random = null;
public function __construct($random)
{
$this->setLatestAdvert($random);
}
public function setLatestAdvert($random)
{
$this->random = $random;
}
public function getLatestAdvert()
{
return $this->random;
}
public function __invoke()
{
$latestAdverts = $this->getLatestAdvert();
return $latestAdverts;
}
}
3. the Factory: Advert\View\Helper\LatestAdvertFactory.php
namespace Advert\View\Helper;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class LatestAdvertFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$locator = $serviceLocator->getServiceLocator();
$service = $locator->get('Advert\Service');
$random = $service->fetchSingleByRandom();
$helper = new LatestAdvert($random);
return $helper;
}
}
4. the Service: Advert\Service\LatestAdvertService .php
namespace Advert\Service;
use Advert\Entity\Advert as AdvertEntity;
use Doctrine\ORM\EntityManager;
use Zend\ServiceManager\ServiceManager;
use Zend\ServiceManager\ServiceManagerAwareInterface;
use Zend\Debug\Debug;
class LatestAdvertService implements ServiceManagerAwareInterface
{
/**
* Service manager.
* #var Zend\ServiceManager\ServiceManager
*/
private $serviceManager = null;
/**
* Sets service manager.
* #param Zend\ServiceManager\ServiceManager $serviceManager Service manager.
*/
public function setServiceManager(ServiceManager $serviceManager)
{
$this->serviceManager = $serviceManager;
}
/**
* Returns service manager.
* #return type
*/
public function getServiceLocator()
{
return $this->serviceManager;
}
public function fetchSingleByRandom()
{
// Get Doctrine entity manager.
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
$advertType = 'wanted'; // This should be removed
$random = $entityManager->getRepository('Advert\Entity\Advert')
->findAdvertsByDate($advertType);
return $random;
}
}
5. Module: Advert\Module.php
public function getServiceConfig()
{
return array(
'invokables' => array(
'Advert\Service' => 'Advert\Service\LatestAdvertService',
),
);
}
public function getViewHelperConfig()
{
return array(
'factories' => array(
'latestAdvert' => 'Advert\View\Helper\LatestAdvertFactory',
),
);
}
As you can see in #4 I have a Variable called $advertType. I want to set the variable when I call the view Helper in my index.html, f.e. $this->latestAdvert('wanted'), but how can I pass this variable through all my files? I just can not find a solution for it. Does anyone got a tip for me how to achieve it? Thank you very much in advance.
!UPDATE!
As SenseException pointed out below, that injecting a service locator into a service is a bad practice and instead I should either inject repository or entity manager into the service, I have now worked out the first working solution for the entity manager.
For that I have updated 2 Files: module.php and LatestAdvertService.php
#5 module.php
public function getServiceConfig()
{
return array(
'factories' => array(
'Advert\Service' => function ($sl) {
$entityManager = $sl->get('doctrine.entitymanager.orm_default');
$myService = new Service\LatestAdvertService();
$myService->setEntityManager($entityManager);
//or you can set repository
//$repository = $entityManager->getRepository('Advert\Entity\Advert');
//$myService->setRepository($repository);
return $myService;
},
4. the Service: Advert\Service\LatestAdvertService .php
namespace Advert\Service;
use Advert\Entity\Advert as AdvertEntity;
use Doctrine\ORM\EntityManager;
class LatestAdvertService
{
public function setEntityManager(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function setRepository(Repository $repository) {
$this->repository = $repository;
}
public function fetchSingleByAdvertType($advertType)
{
$random = $this->entityManager->getRepository('Advert\Entity\Advert')->findAdvertsByDate($advertType);
// $random = $this->repository->findAdvertsByDate($advertType);
return $random;
}
}
I have tried to inject the repository but get the following error message:
Argument 1 passed to Advert\Service\LatestAdvertService::setRepository() must be an instance of Advert\Service\AdvertRepository, instance of Advert\Repository\AdvertRepository given, called in
I will continue to find a solution for the repository injection and update when successful.
How about this solution:
namespace Advert\View\Helper;
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class LatestAdvertFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$locator = $serviceLocator->getServiceLocator();
$service = $locator->get('Advert\Service');
$helper = new LatestAdvert($service);
return $helper;
}
}
And of course the helper class:
namespace Advert\View\Helper;
use Zend\View\Helper\AbstractHelper;
class LatestAdvert extends AbstractHelper
{
protected $service;
public function __construct($service)
{
$this->service = $service;
}
public function __invoke($advertType)
{
$latestAdverts = $this->service->fetchSingleByAdvertType($advertType);
return $latestAdverts;
}
}
And for the service:
public function fetchSingleByAdvertType($advertType)
{
$entityManager = $this->getServiceLocator()
->get('doctrine.entitymanager.orm_default');
$random = $entityManager->getRepository('Advert\Entity\Advert')
->findAdvertsByDate($advertType);
return $random;
}
I tried to keep your code as close to your original as possible but please hear some suggestions about the service locator. It is a bad practice to inject a service locator into a service like you did in LatestAdvertService. Since you only need a repository for your service, just inject that one into it. If you need the entity manager in your service, inject it instead. Your unittests will thank you.

Laravel ioc automatic resolution - works from controller but not from custom class

Namespaces omitted for brevity...
I have written the following service provider and registered in config/app.php:
class OfferServiceProvider extends ServiceProvider
{
public function register()
{
$this->registerLossControlManager();
}
protected function registerLossControlManager()
{
$this->app->bind('LossControlInterface', 'LossControl');
}
}
Here is my LossControlInterface
interface LossControlInterface
{
/**
* #param int $demandId
* #param float $offerTotal
* #param float $productTotal
* #param null|int $partnerId
* #return mixed
*/
public function make($demandId, $offerTotal, $productTotal, $partnerId = null);
/**
* #return float
*/
public function getAcceptableLoss();
/**
* #return bool
*/
public function isAcceptable();
/**
* #return bool
*/
public function isUnacceptable();
/**
* #return null
*/
public function reject();
}
Now within the controller, I can inject the LossController as follows:
use LossControlInterface as LossControl;
class HomeController extends BaseController {
public function __construct(LossControl $lossControl)
{
$this->lossControl = $lossControl;
}
public function getLossThresholds()
{
$lossControl = $this->lossControl->make(985, 1000, null);
var_dump('Acceptable Loss: ' . $lossControl->getAcceptableLoss());
var_dump('Actual Loss: ' . $lossControl->calculateLoss());
var_dump('Acceptable? ' . $lossControl->isAcceptable());
}
}
However if I try to dependency inject the LossControlInterface from within a custom class called by a command:
[2014-09-02 13:09:52] development.ERROR: exception 'ErrorException' with message 'Argument 11 passed to Offer::__construct() must be an instance of LossControlInterface, none given, called in /home/vagrant/Code/.../ProcessOffer.php on line 44 and defined' in /home/vagrant/Code/.../Offer.php:79
It appears as though I am unable to dependency inject the interface into a custom class, but I can when dependency injecting into a controller.
Any thoughts on what Im doing wrong or have omitted to get the automatic resolution working?
The IoC is automatic within controllers, and you don't see the injection because Laravel handles the construction of controllers for you. When creating any other custom class by using the new keyword, you will still need to send in all of the parameters needed to it's constructor:
$myClass = new ClassWithDependency( app()->make('Dependency') );
You can hide this, to a degree, by funneling creation of your custom class through a service provider:
// Your service provider
public function register()
{
$this->app->bind('ClassWithDependency', function($app) {
return new ClassWithDependency( $app->make('Dependency') );
});
}
Then just have the IoC make it whenever you need it:
$myClass = app()->make('ClassWithDepenency');
In your case, you can change your code to look like this:
private function setOffer(Offer $offer = null) {
$this->processOffer = $offer ?:
new Offer( app()->make('LossControlInterface') );
}
A perhaps cleaner approach could be to create a service provider and an OfferFactory which gets injected into your controller. The controller can then request the factory to create the offer whenever it needs one:
// Controller
public function __construct(OfferFactory $offerFactory)
{
$this->offerFactory = $offerFactory;
}
public function setOffer(Offer $offer = null)
{
$this->processOffer = $offer ?: $this->offerFactory->createOffer();
}
// OfferFactory
class OfferFactory
{
public function createOffer()
{
return app()->make('Offer');
}
}
This has the benefit of completely decoupling your controller from the logic behind the creation of the offer, yet allowing you to have a spot to add any amount of complexity necessary to the process of creating offers.
In Laravel 5.2 the simplest solution for your particular problem would be to replace
new Offer();
with
App::make('Offer');
or even shorter
app('Offer');
which will use Laravel Container to take care of dependencies.
If however you want to pass additional parameters to the Offer constructor it is necessary to bind it in your service provider
App::bind('Offer', function($app, $args) {
return new Offer($app->make('LossControl'), $args);
});
And voila, now you can write
app('Offer', [123, 456]);
In laravel 5.4 (https://github.com/laravel/framework/pull/18271) you need to use the new makeWith method of the IoC container.
App::makeWith( 'App\MyNameSpace\MyClass', [ $id ] );
if you still use 5.3 or below, the above answers will work.

Symfony2 / FOSUserBundle: Change render variables in response without altering parent class

One of my classes currently extends the BaseController on the FOSUserBundle, and returns the parent action. However, due to project spec, I shouldn't have the need to edit the parent class. Is there a way of sending additional variables, for twig to render, through the child response?
Child Class:
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
return $response; // and 'myVariable' => $myVariable
}
}
Parent Class:
class ChangePasswordController extends ContainerAware
{
/**
* Change user password
*/
public function changePasswordAction(Request $request)
{
//lots of code.....
return $this->container->get('templating')
->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'
.$this->container->getParameter('fos_user.template.engine'),
array(
'form' => $form->createView()
//and 'myVariable' => $myVariable
)
);
}
}
So to summarise, is there a way of passing something to the parent class, without changing the parent class... whilst rendering the twig view with an additional variable.
-- Update --
Essentially I want to render a form using the FOSUserBundle changePassword action, therefore this works fine:
return $this->container
->get('templating')
->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'.$this->container->getParameter('fos_user.template.engine'),
array('form' => $form->createView())
);
However, I want to pass more variables to the view, just like the 'form' is passed as shown above, without altering the FosUserBundle ChangePassword Controller. Therefore I have a class which inherits the that controller, adds some additional functionality and returns the parent change password action:
class ChangePassController extends ChangePasswordController
{
public function changePasswordAction(Request $request)
{
// more code......
$response = parent::changePasswordAction($request);
return $response;
}
}
But, like with most applications, I want to add more than just the form variable to a view template. So is there a way of passing an additional variable to the view, without altering the parent controller / action? Like (but not like) pushing 'myVariable' => $myVariable to the parent changePasswordAction return statement?
There is a section in FOSUserBundle documentation that describes exactly how to do that, and from Symfony2's Cookbook, How to use Bundle Inheritance to Override parts of a Bundle.
In summary, create a Bundle class to override FOSUserBundle in src:
// src/Acme/UserBundle/AcmeUserBundle.php
<?php
namespace Acme\UserBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class AcmeUserBundle extends Bundle
{
public function getParent()
{
return 'FOSUserBundle';
}
}
Then, override the ChangePasswordController class:
use FOS\UserBundle\Controller\ChangePasswordController as BaseController;
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
return $response; // and 'myVariable' => $myVariable
}
}
--UPDATE--
Ok I think I misread you question. Anyway what renderResponse() of the templating service does is essentially:
$response->setContent($this->render($view, $parameters));
You can see the Class of the templating service by running app/console container:debug which is actually the TwigEngine class.
So you can just re-invoke renderResponse() and supply you own extra parameters. eg:
return $this->container->get('templating')->renderResponse(
'FOSUserBundle:ChangePassword:changePassword.html.'.$this->container->getParameter('fos_user.template.engine'),
array(
'form' => $form->createView(),
'myVariable' => $myVariable', // There you go
),
$response // The previous response that has been rendered by the parent class, by this is not necessary
);
Think bottom up.
You can access your data without passing it through action, using Twig Extension http://symfony.com/doc/current/cookbook/templating/twig_extension.html
twig.extension.user_profile:
class: 'MyBundle\UserProfileExtension'
arguments:
- '#doctrine.orm.entity_manager'
tags:
- { name: twig.extension }
Extension class
class UserProfileExtension extends \Twig_Extension
{
/**
* #var EntityManager
*/
private $entityManager;
/**
* #param UserProfileDataService $userProfileDataService
*/
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* #return array
*/
public function getFunctions()
{
return array(
new \Twig_SimpleFunction('get_my_custom_var', array($this, 'getMyCustomVar')),
);
}
/**
* #return array
*/
public function getMyCustomVar()
{
$var = $this->entityManager->getRepository('MyCustomRepository')->findOneBy(['id' => 1]);
return $var;
}
/**
* Returns the name of the extension.
*
* #return string The extension name
*/
public function getName()
{
return 'user_profile_extension';
}
Template usage
{dump(get_my_custom_var())}
if I am understanding your question correctly you should be able to set additional variables on the response like this:
use FOS\UserBundle\Controller\ChangePasswordController as BaseController;
class ChangePasswordController extends BaseController
{
public function changePasswordAction(Request $request)
{
$response = parent::changePasswordAction($request);
$response['myVariable'] = $myVariable;
return $response;
}
}
Hope this helps!

Categories