Grouped routes in multiple controllers in Symfony 4 - php

I'm rewriting my Silex-based application to Symfony 4, as the Silex will be deprecated in a while from now. Everything works great so far, but I have a problem with nested routes.
I had lots of nested (child routes) in Silex application with different controllers assigned to them.
$app->match('/api', function (ControllerCollection $api) {
$api->get('/homepage', 'ControllerOne::index');
$api->get('/contact', 'ControllerTwo::index');
});
This was pretty easy in Silex, but now in Symfony 4, I'm using annotations for the routes' management and it seems like I can't find a way to group those routes.
It's annoying especially when it comes to routes with _locale as the syntax for those routes is pretty long and still.. it's not a good way to have it everywhere in case I need to change the _locale prefix some day to something like /home/{_locale}/.
ControllerOne extends Controller
{
/**
* #Route("/{_locale}/",
* name="root",
* methods="GET",
* requirements={"_locale": "en|fr"}
* )
*
* #return Response
*/
public function index(): Response
{
return $this->render('some.html.twig');
}
}
ControllerTwo extends Controller
{
/**
* #Route("/{_locale}/homepage",
* name="homepage",
* methods="GET",
* requirements={"_locale": "en|fr"}
* )
*
* #return Response
*/
public function index(): Response
{
return $this->render('some2.html.twig');
}
}
UPDATE
I had an idea to create some sort of PrefixedController where I'd specify the prefix over the class and the extend that PrefixedController instead of the basic Controller, but it seems to don't work.
/**
* #Route("/{_locale}", requirements={"_locale": "en|fr"})
*/
controller PrefixedController extends Controller
{
}
controller ControllerOne extends PrefixedController
{
/**
* #Route("/", methods="GET")
* #Return Response
*/
public function index(): Response
{
return $this->render('some.html.twig');
}
}
But when I navigate to /en/ it can't match the route.

This can be done in the main routing file where the routing resources are imported. In Symfony 4 it is in config/routes/annotations.yaml. Then to provide a prefix /{_locale} for the imported routes uses the prefix option:
# config/routes/annotations.yaml
controllers:
resource: '../src/Controller/'
type: annotation
prefix: /{_locale}
The path of each route being loaded from the new routing resource will now be prefixed with the placeholder /{_locale}.

Related

Symfony 3: cannot access the container from inside a controller

I'm migrating my app from Symfony 2.8 to Symfony 3.3.
From inside a controller of mine I have this:
public function indexAction()
{
$email = new Email();
$form = $this->createForm(GetStartedType::class, $email, [
'action' => $this->generateUrl('get_started_end'),
'method' => 'POST',
]);
return [
'form' => $form->createView(),
];
}
But I receive this exception:
Call to a member function get() on null
My controller extends Symfony\Bundle\FrameworkBundle\Controller\Controller:
/**
* {#inheritdoc}
*/
class DefaultController extends Controller
{
...
}
So I have access to the container.
Putting some dumps around in the Symfony's code, I see that the container is correctly set:
namespace Symfony\Component\DependencyInjection;
/**
* ContainerAware trait.
*
* #author Fabien Potencier <fabien#symfony.com>
*/
trait ContainerAwareTrait
{
/**
* #var ContainerInterface
*/
protected $container;
/**
* Sets the container.
*
* #param ContainerInterface|null $container A ContainerInterface instance or null
*/
public function setContainer(ContainerInterface $container = null)
{
dump('Here in the ContainerAwareTrait');
dump(null === $container);
$this->container = $container;
}
}
This dumps
Here in the ContainerAwareTrait
false
So the autowiring works well and sets the container.
But in the ControllerTrait I have this:
trait ControllerTrait
{
/**
* Generates a URL from the given parameters.
*
* #param string $route The name of the route
* #param mixed $parameters An array of parameters
* #param int $referenceType The type of reference (one of the constants in UrlGeneratorInterface)
*
* #return string The generated URL
*
* #see UrlGeneratorInterface
*/
protected function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
dump('Here in the ControllerTrait');
die(dump(null === $this->container));
return $this->container->get('router')->generate($route, $parameters, $referenceType);
}
...
this is the dump:
Here in the ControllerTrait
true
So here the container is null and this causes the error.
Anyone can help me solve this issue?
Why is the container null?
If may help, this is the services.yml configuration (the default that cames with Symfony):
# controllers are imported separately to make sure they're public
# and have a tag that allows actions to type-hint services
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
tags: ['controller.service_arguments']
This question is posted as issue on the Symfony's issue tracker.
The S3.3 autowire capability makes it a bit easier to define controllers as services.
The usual motivation behind defining controllers as services is to avoid injecting the container. In other words you should be explicitly injecting each service a controller uses. The autowire capability allows you to use action method injection so you don't have to inject a bunch of stuff in the constructor.
However, the base Symfony controller class provides a number of helper function which use about 12 different services. It would be painful indeed to inject these one at a time. I had sort of thought that the autowire capability might take care of this for you but I guess not.
So you basically need to add a call to setContainer in your service definition. Something like:
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
public: true
[[setContainer, ['#service_container']]]
tags: ['controller.service_arguments']
The autowire capability is very much a work in progress so I would not be surprised if this changes for 3.4/4.0.
This problem is fixed by PR #23239 and is relased in Symfony 3.3.3.

Imitating Drupal url aliases in Laravel 5.2

In Drupal there is a simple url rewrite system that stores path aliases and the real route in the database.
For example:
/category/hello => node/5
I would like to imitate this system in Laravel.
I know how to create the database structure. What I would like suggestions for is actually overriding and remapping the incoming request.
I've taken a glance at the router. No events are really sticking out. What I would like to avoid is adding every permutation as a static route. I would like to for this to be completely dynamic.
I was reading middleware with a redirect would work but don't know if that is the best route to go. Keep in mind that the aliases could be anything. There isn't any set pattern.
The actual business case for this is the application has a hierarchy of categories like for a catalog on an ecommerce site. For every path a dynamic page will need to exist and possibly also allow pass-thrus to other pages.
Ex.
/sports/football/nfl => \App\Http\Controllers\Category::lp(2)
Even something like:
/sports/football/nfl/:game/lines => \App\Http\Controllers\Lines::lp(:game)
However, I don't want to have every permutation in the db. Just the base one and allow everything after /sports/football/nfl/* pass thru to a completely different location.
If I do recall in Symfony this could be done with a custom route matcher. However, I don't see anything like that in Laravel. Unless I'm just missing something. It looks like you either add a static route or nothing all but I haven't taken the deep dive into that code yet so could be wrong.
I was able to implement a dynamic routing system by creating my own custom route and adding to the route collection manually.
Custom Route
use Illuminate\Routing\Route as BaseRoute;
use Modules\Catalog\Routing\Matching\CategoryValidator;
use Illuminate\Routing\Matching\MethodValidator;
use Illuminate\Routing\Matching\SchemeValidator;
use Illuminate\Routing\Matching\HostValidator;
use Illuminate\Http\Request;
use Modules\Event\Repositories\CategoryRepository;
use Illuminate\Routing\ControllerDispatcher;
/**
* Special dynamic touting for catalog categories.
*/
class CategoryRoute extends BaseRoute {
protected $validatorOverrides;
/**
* #param CategoryRepository
*/
protected $categoryRepository;
/**
* Create a new Route instance.
*
* #param CategoryRepository $categoryRepository
* The category repository.
*/
public function __construct(CategoryRepository $categoryRepository)
{
$this->categoryRepository = $categoryRepository;
$action = [
'uses'=> function() use ($categoryRepository) {
$path = app('request')->path();
$category = $categoryRepository->findOneByHierarchicalPath($path);
$controller = app()->make('Modules\Catalog\Http\Controllers\Frontend\CategoryController');
return $controller->callAction('getIndex', ['categoryId'=>$category->getId()]);
}
];
$action['uses']->bindTo($this);
parent::__construct(['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE'],'_catalog_category',$action);
}
/**
* Determine if the route matches given request.
*
* #param \Illuminate\Http\Request $request
* #param bool $includingMethod
* #return bool
*/
public function matches(Request $request, $includingMethod = true)
{
$this->compileRoute();
$validators = $this->getValidatorOverrides();
foreach ($validators as $validator) {
/*if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}*/
if (! $validator->matches($this, $request)) {
return false;
}
}
return true;
}
/**
* Get the route validators for the instance.
*
* #return array
*/
public function getValidatorOverrides()
{
if (isset($this->validatorOverrides)) {
return $this->validatorOverrides;
}
$this->validatorOverrides = [
new MethodValidator, new SchemeValidator,
new HostValidator, /*new UriValidator,*/
new CategoryValidator($this->categoryRepository)
];
return $this->validatorOverrides;
}
}
Custom Route Validator
<?php
namespace Modules\Catalog\Routing\Matching;
use Illuminate\Routing\Matching\ValidatorInterface;
use Illuminate\Routing\Route;
use Illuminate\Http\Request;
use Modules\Event\Repositories\CategoryRepository;
class CategoryValidator implements ValidatorInterface
{
protected $categoryRepository;
public function __construct(CategoryRepository $categoryRepository) {
$this->categoryRepository = $categoryRepository;
}
/**
* Validate a given rule against a route and request.
*
* #param \Illuminate\Routing\Route $route
* #param \Illuminate\Http\Request $request
* #return bool
*/
public function matches(Route $route, Request $request)
{
$path = $request->path() == '/' ? '/' : '/'.$request->path();
$category = $this->categoryRepository->findOneByHierarchicalPath($path);
return $category?true:false;
}
}
To satisfy the requirements of the category repository dependency I had to also create a subscriber that adds the route after all the providers had been booted. Simply placing it in the routes.php file would not work because there was no guarantee that all the dependencies for IoC would be configured when that file gets loaded.
Bootstrap Subscriber
use Modules\Catalog\Routing\CategoryRoute;
use Modules\Event\Repositories\CategoryRepository;
use Illuminate\Support\Facades\Route as RouteFacade;
class BootstrapSubscriber {
public function subscribe($events) {
$events->listen(
'bootstrapped: Illuminate\Foundation\Bootstrap\BootProviders',
'Modules\Catalog\Subscribers\BootstrapSubscriber#onBootstrappedBootProviders'
);
}
public function onBootstrappedBootProviders($event) {
$categoryRepository = app(CategoryRepository::class);
RouteFacade::getRoutes()->add(new CategoryRoute($categoryRepository));
}
}
I will probably expand on this but that is the basic way to do it.

Using ParamConverters with relations

I'm trying to match a GitHub-style URL (/{user}/{project}) using Symfony2 #ParamConverters. They retrieve the correct entities, however I would like to ensure that the project belongs to the user in the URL. There is a Doctrine relation between the two entities.
For example, with a Project 'bar', belonging to 'foo', I can access it at /foo/bar. However I can also access it under a different user: /baz/bar.
Is it possible to do this using the ParamConverter, or do I need to manually check in the action?
/**
* #Route("/{user}")
* #ParamConverter("user", class="AcmeUserBundle:User", options={"mapping": {"user": "usernameCanonical"}})
*/
class ProjectController extends Controller
{
/**
* #Route("/{project}")
* #ParamConverter("project", class="AcmeProjectBundle:Project", options={"mapping": {"project": "slug"}})
* #Template()
*/
public function showAction(User $user, Project $project)
{
// Can I automate this check?
if ($user->getId() !== $project->getOwner()->getId()) {
throw $this->createNotFoundException();
}
}
}
Found the solution to this, largely thanks to #Qoop's comment. By adding "user": "owner" to the mapping, the owner relation is queried using the $user variable (which because of the first #ParamConverter, is already a User instance.
Under-the-hood, this issues two queries - one to retrieve the first user and the second to retrieve the project (with project.owner_id = user.id and project.slug = slug). However I assume these are cacheable at the Doctrine-level.
The result of accessing the (non-existant) /baz/bar is a 404 with the message:
AcmeProjectBundle:Project object not found.
/**
* #Route("/{user}")
* #ParamConverter("user", class="AcmeUserBundle:User", options={"mapping": {"user": "usernameCanonical"}})
*/
class ProjectController extends Controller
{
/**
* #Route("/{project}")
* #ParamConverter("project", class="AcmeProjectBundle:Project", options={"mapping": {"user": "owner", "project": "slug"}})
* #Template()
*/
public function showAction(User $user, Project $project)
{
// Do something with $user and $project,
// where $project->getOwner() === $user
}
}

zf2 mvc event-listener or strategy

im new to Zf2, i recently upgraded from zf1 and need help and advice on this problem.
Here the fact :
I'm working on medical project (which is an upgrade to a zf1 version) in some controller (page) i need to have the patient's info and current visitation in sidebar panel...
I know i'm new to zf2 but i don't want to do redundant things like having in every action the getvisiteService() and patientService() retrieve info and passing these results to view over and over.
I thought about a plugin but again i have to pass from controller to view and supercharge my view with partials and placeholder helper (grr!!!)
Thinkin' about Strategy and eventlistener but i don't know how these work and i need to inject result to a partial.
So there is a simple and/or complicated way to achieve that? Thank you in advance any hint and code will be appreciated and sorry for my poor english i speak french (such a typical excuse :) )
There's a ton of approaches you could use here, but sticking to your original question, it's quite easy to inject things into your layout model, with something like this:
Module.php
/**
* On bootstrap event
*
* #param \Zend\Mvc\MvcEvent $e
*/
public function onBootstrap(MvcEvent $e)
{
// Inject something, like a nav into your Layout view model
$viewModel = $e->getViewModel(); // Layout View Model
$navigation= new ViewModel(array(
'username' => 'Bob' // Dynamically set some variables..
));
$navigation->setTemplate('navigation/mynav');
$viewModel->addChild($navigation, 'navigation');
}
You could also create a custom view Helper to do the work for you if you wanted
<?php
/**
* MyHelper.php
*/
namespace Application\View\Helper;
use Zend\View\Helper\AbstractHelper;
use Zend\ServiceManager\ServiceManagerAwareInterface;
use Zend\ServiceManager\ServiceManager;
class MyHelper extends AbstractHelper implements ServiceManagerAwareInterface
{
/**
* Invoke
*
* #return string
*/
public function __invoke()
{
// Dynamically build your nav or what ever.
$patientService = $this->getServiceManager()->get('PatientService');
return 'soemthing';
}
/**
* #var ServiceManager
*/
protected $serviceManager;
/**
* Retrieve service manager instance
*
* #return ServiceManager
*/
public function getServiceManager()
{
return $this->serviceManager;
}
/**
* Set service manager instance
*
* #param ServiceManager $locator
* #return User
*/
public function setServiceManager(ServiceManager $serviceManager)
{
$this->serviceManager = $serviceManager;
return $this;
}
}

How can I have optional parameters in Symfony2 route?

I have this code below:
/**
* Lists all User entities.
*
* #Route("/{cid}",defaults={"cid" = null},name="user")
* #Template()
*/
public function indexAction($cid=null)
{}
Now if I type site/user/1 then it works, but if I type site/user/ it says:
No route found
How can I have it that both routes work?
Try to go to site/user (notice no backslash at the end).
Generally it should work, I have relatively similar configuration working.
But if all else fails you can always define multiple routes for same action, i.e.
/**
* Lists all User entities.
*
* #Route("/", name="user_no_cid")
* #Route("/{cid}", name="user")
* #Template()
*/
public function indexAction($cid=null)
{
Use a yml file for your routing configuration, and add a default value for id in your routing parameters like this:
user:
pattern: /site/user/{id}
defaults: { _controller: YourBundle:Default:index, id: 1 }
See documentation here
You could also do it with a GET parameter, e.g.
/**
* #param Request $request
*
* #return Response
*/
public function displayDetailAction(Request $request) : Response
{
if ($courseId = $request->query->get('courseId')) {

Categories