I know that the basis of Silex approach in which all the application logic in a single file. But my application will be possible to have more than twenty controllers. So I want to have a handy map to manage the router.
My question is to search for solutions in which I would be able to make a router to a separate file. In the best case, the file must be of YAML type:
# config/routing.yml
_home:
pattern: /
defaults: { _controller: MyProject\Controller\MyController::index }
But the native is also a good case (for me):
$routes = new RouteCollection();
$routes->add(
'home',
new Route('/', array('controller' => 'MyProject\Controller\MyController::index')
));
return $routes;
Problem of the second case is that I have to use the match() function for each rule of routing. It is not at all clear.
What are the ways to solve this issue? The condition is that I want to use the existing API Silex or components of Symfony2.
Small note:
I don't use a ControllerProviderInterface for my Controller classes. This is an independent classes.
First of all, the basis of Silex is not that you put everything in one file. The basis of Silex is that you create your own 'framework', your own way of organizing applications.
"Use silex if you are comfortable with making all of your own architecture decisions and full stack Symfony2 if not."
-- Dustin Whittle
Read more about this in this blogpost, created by the creator of Silex.
How to solve your problem
What you basically want is to parse a Yaml file and get the pattern and defaults._controller settings from each route that is parsed.
To parse a Yaml file, you can use the Yaml Component of Symfony2. You get an array back which you can use to add the route to Silex:
// parse the yaml file
$routes = ...;
$app = new Silex\Application();
foreach ($routes as $route) {
$app->match($route['pattern'], $route['defaults']['_controller']);
}
// ...
$app->run();
I thought I'd add my method here as, although others may work, there isn't really a simple solution. Adding FileLocator / YamlFileLoader adds a load of bulk that I don't want in my application just to read / parse a yaml file.
Composer
First, you're going to need to include the relevant files. The symfony YAML component, and a really simple and useful config service provider by someone who actively works on Silex.
"require": {
"symfony/yaml": "~2.3",
"igorw/config-service-provider": "1.2.*"
}
File
Let's say that your routes file looks like this (routes.yml):
config.routes:
dashboard:
pattern: /
defaults: { _controller: 'IndexController::indexAction' }
method: GET
Registration
Individually register each yaml file. The first key in the file is the name it will be available under your $app variable (handled by the pimple service locator).
$this->register(new ConfigServiceProvider(__DIR__."/../config/services.yml"));
$this->register(new ConfigServiceProvider(__DIR__."/../config/routes.yml"));
// any more yaml files you like
Routes
You can get these routes using the following:
$routes = $app['config.routes']; // See the first key in the yaml file for this name
foreach ($routes as $name => $route)
{
$app->match($route['pattern'], $route['defaults']['_controller'])->bind($name)->method(isset($route['method'])?$route['method']:'GET');
}
->bind() allows you to 'name' your urls to be used within twig, for example.
->method() allows you to specify POST | GET. You'll note that I defaulted it to 'GET' with a ternary there if the route doesn't specify a method.
Ok, that's how I solved it.
This method is part of my application and called before run():
# /src/Application.php
...
protected function _initRoutes()
{
$locator = new FileLocator(__DIR__.'/config');
$loader = new YamlFileLoader($locator);
$this['routes'] = $loader->load('routes.yml');
}
Application class is my own and it extends Silex\Application.
Configuration file:
# /src/config/routes.yml
home:
pattern: /
defaults: { _controller: '\MyDemoSite\Controllers\DefaultController::indexAction' }
It works fine for me!
UPD:
I think this is the right option to add collections:
$this['routes']->addCollection($loader->load('routes.yml'));
More flexible.
You could extend the routes service (which is a RouteCollection), and load a YAML file with FileLocator and YamlFileLoader:
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
$app->extend('routes', function($routeCollection) {
$locator = new FileLocator([__DIR__ . '/../config']);
$loader = new YamlFileLoader($locator);
$collection = $loader->load('routes.yml');
$routeCollection->addCollection($collection);
return $routeCollection;
});
You will need symfony/config and symfony/yaml dependencies though.
Related
I'm trying to create a reusable bundle to manage basket in webstore application...
I create every desired action in a reusable bundle using a controller like addBasketAction, removeBasketAction and so on... The classical
Everythings works fine, but now I'm litteraly stuck about how to "call" my bundle from my "principal" application
You dont call a bundle but you have to register your bundle in the AppKernel.php file:
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
// ...
new AppBundle\AppBundle(),
new Company\CustomBundle\CompanyCustomBundle(),
];
}
// ...
}
Then you will need to import your routes too but i dont know if you use annotations for your routes or a yaml file.
read more:
http://symfony.com/doc/current/bundles.html
http://symfony.com/doc/current/bundles/best_practices.html
[edit]
Reading your topic title it looks like that you are confusing bundles and services. You can add a service in your bundle on the same way as you can add this service in your AppBundle:
http://symfony.com/doc/current/service_container.html#creating-configuring-services-in-the-container
I was looking for Routing Component's documentation, explicitly which types of parameters it accepts.
For example, the type: annotations in my just created app's routing.yml made me want to look what other types are out there, but there is no docs on it. I can only find the documentation in the Book and a little bit in Components.
Loader types
Main types of routing loaders are described in the component's docs. It mentions quite a few loaders:
YamlFileLoader
XmlFileLoader
PhpFileLoader
ClosureLoader
AnnotationFileLoader, AnnotationClassLoader & AnnotationDirectoryLoader
You'll find all the core loaders in the Symfony\Component\Routing\Loader namespace
It's all based on Config's component loaders, so it's worth if you also read about the Config component.
Each loader's supports() method will tell you in which circumstances the loader is actually used. For example, for the YamlFileLoader it's:
public function supports($resource, $type = null)
{
return is_string($resource)
&& 'yml' === pathinfo($resource, PATHINFO_EXTENSION)
&& (!$type || 'yaml' === $type);
}
You can see it looks at resource's extension and type.
Custom loaders
You can implement your own loaders by implementing the Symfony\Component\Config\Loader\LoaderInterface.
Read more about it in the How to Create a custom Route Loader cookbook. It actually explains quite a lot on how routing loaders work. Have a look at some 3rd party loaders too, such as the FOSRestBundle's one.
How to wire it all together
Have a look at the generated container in the Symfony Standard Edition to see how the full stack framework wires it all together. It should look similar to:
/**
* Gets the 'routing.loader' service.
*
* This service is shared.
* This method always returns the same instance of the service.
*
* #return \Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader A Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader instance.
*/
protected function getRouting_LoaderService()
{
$a = $this->get('file_locator');
$b = $this->get('annotation_reader');
$c = new \Sensio\Bundle\FrameworkExtraBundle\Routing\AnnotatedRouteControllerLoader($b);
$d = new \Symfony\Component\Config\Loader\LoaderResolver();
$d->addLoader(new \Symfony\Component\Routing\Loader\XmlFileLoader($a));
$d->addLoader(new \Symfony\Component\Routing\Loader\YamlFileLoader($a));
$d->addLoader(new \Symfony\Component\Routing\Loader\PhpFileLoader($a));
$d->addLoader(new \Symfony\Component\Routing\Loader\AnnotationDirectoryLoader($a, $c));
$d->addLoader(new \Symfony\Component\Routing\Loader\AnnotationFileLoader($a, $c));
$d->addLoader($c);
return $this->services['routing.loader'] = new \Symfony\Bundle\FrameworkBundle\Routing\DelegatingLoader($this->get('controller_name_converter'), $this->get('monolog.logger.router', ContainerInterface::NULL_ON_INVALID_REFERENCE), $d);
}
The key here is the LoaderResolver which takes care of finding an appropriate loader for a type of configuration.
My config is
jms_serializer:
metadata:
auto_detection: true
directories:
NameOfBundle:
namespace_prefix: ""
path: "#VendorNameOfBundle/Resources/config/serializer"
My YML file named Entity.Project.yml contains
Vendor\NameOfBundle\Entity\Project:
exclusion_policy: ALL
properties:
id:
expose: true
I am loading the serializer like so from within a Controller
$serializer = SerializerBuilder::create()
->configureListeners(function(EventDispatcher $dispatcher) {
$dispatcher->addSubscriber(new ProjectSubscriber($this->container));
})
->addDefaultListeners()
->build();
This completely ignored my YML file and exposes all fields from the Project. I have cleared the cache.
But if I use this instead without the custom subscriber, then the exclusions work
$serializer = $this->get("jms_serializer");
Even explicitly adding a dir does not work either
$serializer = SerializerBuilder::create()
->configureListeners(function(EventDispatcher $dispatcher) {
$dispatcher->addSubscriber(new ProjectSubscriber($this->container));
})
->addDefaultListeners()
->addMetadataDir(realpath($this->get('kernel')->getRootDir()."/../") . '/src/Vendor/NameOfBundle/Resources/config/serializer')
->build();
The docs are not clear on how this path should befined. The above method does not error, but does not pull in the YML files. The below method errors and says the directory does not exist;
$serializer = SerializerBuilder::create()
->configureListeners(function(EventDispatcher $dispatcher) {
$dispatcher->addSubscriber(new ProjectSubscriber($this->container));
})
->addDefaultListeners()
->addMetadataDir('#VendorNameOfBundle/Resources/config/serializer')
->build();
How do I make the JMS Serializer look at my YML file in order to exclude the fields and also use the Subscriber?
As i see from documentation you need to setup your Yaml files:
it is necessary to configure a metadata directory where those files are located:
$serializer =
JMS\Serializer\SerializerBuilder::create()
->addMetadataDir($someDir)
->build();
For more information read manual.
This was helpful Using JMSSerialize to serialize Doctrine2 Entities that follow SimplifiedYamlDriver convention
It would appear that the file names needs to be completely different if you do not specify a namespace. I never thought to specify a namespace as this is not mentioned in the main docs.
If there is no namespace then the addMetaDir usage is fine but you also need to make sure your file names look like this
Vendor.NameOfBundle.Entity.Project.yml
I have a City parameter stored in a cookie. I would like to include its value as a pattern prefix in my routing configuration like so:
# MyBundle/Resources/config/routing.yml
MyBundle_hotel:
resource: "#MyBundle/Resources/config/routing/hotel.yml"
prefix: /%cityNameFromCookie%/hotel
How can I achieve that?
Give us a use case on how you would want this to work because I don't see the difficulty. Routes are made of parameters that you can specify through the generateUrl function, the url twig function or the path twig function.
In Twig you can do this
{{ path('MyBundle_hotel', {cityNameFromCookie: app.request.cookies.get('cityNameFromCookie')}) }}
In a controller action
$cookieValue = $this->get('request')->cookies->get('cityNameFromCookie');
$url = $this->generateUrl('MyBundle_hotel', array('cityNameFromCookie' => $cookieValue));
Or from any places that have access to the container
$cookieValue = $this->container->get('request')->cookies->get('cityNameFromCookie');
$url = $this->container->get('router')->generate('MyBundle_hotel', array('cityNameFromCookie' => $cookieValue));
In the last example, you will probably want to change how the container is being accessed.
If you are concerned about how complicated it looks like, you can abstract this logic and put it inside a service or extend the router service.
You can find documentation about services and the service container in the Symfony's documentation.
You can also list the services via the command php app/console container:debug and will find the router service and its namespace and from this you can try to figure out how to extend the router service (a very good way to learn how services work).
Otherwise, here is simple way to create a service.
In your services.yml (either in your Bundle or in app/config/config.yml)
services:
city:
class: MyBundle\Service\CityService
arguments: [#router, #request]
In your CityService class
namespace MyBundle\Service
class CityService
{
protected $router;
protected $request;
public function __construct($router, $request)
{
$this->router = $router;
$this->request = $request;
}
public function generateUrl($routeName, $routeParams, $absoluteUrl)
{
$cookieValue = $this->request->cookies->get('cityNameFromCookie');
$routeParams = array_merge($routeParams, array('cityNameFromCookie' => $cookieValue));
return $this->router->generateUrl($routeName, $routeParams, $absoluteUrl);
}
}
Anywhere you have access to the container, you will be able to do the following
$this->container->get('city')->generateUrl('yourroute', $params);
If you still think that it isn't a great solution; you will have to extend the router service (or find a better way to extend the router component to make it behave the way you are expecting it to).
I personally use the method above so I can pass an entity to a path method in Twig. You can find an example in my MainService class and PathExtension Twig class defined in the services.yml.
In Twig, I can do forum_path('routename', ForumEntity) and in a container aware environment I can do $this->container->get('cornichon.forum')->forumPath('routename', ForumEntity).
You should have enough information to make an informed decision
I have added a setting to my config.yml file as such:
app.config:
contact_email: somebody#gmail.com
...
For the life of me, I can't figure out how to read it into a variable. I tried something like this in one of my controllers:
$recipient =
$this->container->getParameter('contact_email');
But I get an error saying:
The parameter "contact_email" must be
defined.
I've cleared my cache, I also looked everywhere on the Symfony2 reloaded site documentation, but I can't find out how to do this.
Probably just too tired to figure this out now. Can anyone help with this?
Rather than defining contact_email within app.config, define it in a parameters entry:
parameters:
contact_email: somebody#gmail.com
You should find the call you are making within your controller now works.
While the solution of moving the contact_email to parameters.yml is easy, as proposed in other answers, that can easily clutter your parameters file if you deal with many bundles or if you deal with nested blocks of configuration.
First, I'll answer strictly the question.
Later, I'll give an approach for getting those configs from services without ever passing via a common space as parameters.
FIRST APPROACH: Separated config block, getting it as a parameter
With an extension (more on extensions here) you can keep this easily "separated" into different blocks in the config.yml and then inject that as a parameter gettable from the controller.
Inside your Extension class inside the DependencyInjection directory write this:
class MyNiceProjectExtension extends Extension
{
public function load( array $configs, ContainerBuilder $container )
{
// The next 2 lines are pretty common to all Extension templates.
$configuration = new Configuration();
$processedConfig = $this->processConfiguration( $configuration, $configs );
// This is the KEY TO YOUR ANSWER
$container->setParameter( 'my_nice_project.contact_email', $processedConfig[ 'contact_email' ] );
// Other stuff like loading services.yml
}
Then in your config.yml, config_dev.yml and so you can set
my_nice_project:
contact_email: someone#example.com
To be able to process that config.yml inside your MyNiceBundleExtension you'll also need a Configuration class in the same namespace:
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root( 'my_nice_project' );
$rootNode->children()->scalarNode( 'contact_email' )->end();
return $treeBuilder;
}
}
Then you can get the config from your controller, as you desired in your original question, but keeping the parameters.yml clean, and setting it in the config.yml in separated sections:
$recipient = $this->container->getParameter( 'my_nice_project.contact_email' );
SECOND APPROACH: Separated config block, injecting the config into a service
For readers looking for something similar but for getting the config from a service, there is even a nicer way that never clutters the "paramaters" common space and does even not need the container to be passed to the service (passing the whole container is practice to avoid).
This trick above still "injects" into the parameters space your config.
Nevertheless, after loading your definition of the service, you could add a method-call like for example setConfig() that injects that block only to the service.
For example, in the Extension class:
class MyNiceProjectExtension extends Extension
{
public function load( array $configs, ContainerBuilder $container )
{
$configuration = new Configuration();
$processedConfig = $this->processConfiguration( $configuration, $configs );
// Do not add a paramater now, just continue reading the services.
$loader = new YamlFileLoader( $container, new FileLocator( __DIR__ . '/../Resources/config' ) );
$loader->load( 'services.yml' );
// Once the services definition are read, get your service and add a method call to setConfig()
$sillyServiceDefintion = $container->getDefinition( 'my.niceproject.sillymanager' );
$sillyServiceDefintion->addMethodCall( 'setConfig', array( $processedConfig[ 'contact_email' ] ) );
}
}
Then in your services.yml you define your service as usual, without any absolute change:
services:
my.niceproject.sillymanager:
class: My\NiceProjectBundle\Model\SillyManager
arguments: []
And then in your SillyManager class, just add the method:
class SillyManager
{
private $contact_email;
public function setConfig( $newConfigContactEmail )
{
$this->contact_email = $newConfigContactEmail;
}
}
Note that this also works for arrays instead of scalar values! Imagine that you configure a rabbit queue and need host, user and password:
my_nice_project:
amqp:
host: 192.168.33.55
user: guest
password: guest
Of course you need to change your Tree, but then you can do:
$sillyServiceDefintion->addMethodCall( 'setConfig', array( $processedConfig[ 'amqp' ] ) );
and then in the service do:
class SillyManager
{
private $host;
private $user;
private $password;
public function setConfig( $config )
{
$this->host = $config[ 'host' ];
$this->user = $config[ 'user' ];
$this->password = $config[ 'password' ];
}
}
I have to add to the answer of douglas, you can access the global config, but symfony translates some parameters, for example:
# config.yml
...
framework:
session:
domain: 'localhost'
...
are
$this->container->parameters['session.storage.options']['domain'];
You can use var_dump to search an specified key or value.
In order to be able to expose some configuration parameters for your bundle you should consult the documentation for doing so. It's fairly easy to do :)
Here's the link: How to expose a Semantic Configuration for a Bundle
Like it was saying previously - you can access any parameters by using injection container and use its parameter property.
"Symfony - Working with Container Service Definitions" is a good article about it.
I learnt a easy way from code example of http://tutorial.symblog.co.uk/
1) notice the ZendeskBlueFormBundle and file location
# myproject/app/config/config.yml
imports:
- { resource: parameters.yml }
- { resource: security.yml }
- { resource: #ZendeskBlueFormBundle/Resources/config/config.yml }
framework:
2) notice Zendesk_BlueForm.emails.contact_email and file location
# myproject/src/Zendesk/BlueFormBundle/Resources/config/config.yml
parameters:
# Zendesk contact email address
Zendesk_BlueForm.emails.contact_email: dunnleaddress#gmail.com
3) notice how i get it in $client and file location of controller
# myproject/src/Zendesk/BlueFormBundle/Controller/PageController.php
public function blueFormAction($name, $arg1, $arg2, $arg3, Request $request)
{
$client = new ZendeskAPI($this->container->getParameter("Zendesk_BlueForm.emails.contact_email"));
...
}
Inside a controller:
$this->container->getParameter('configname')
to get the config from config/config.yaml:
parameters:
configname: configvalue