Access Service from Controller and/or Twig template - symfony

Disclaimer: I'm slowly starting to get into Symfony and still have some problems understanding how the architecture works.
Currently I set up different Bundles (Services, right?) that should deliver different output for different routes. So far I got around adding a simple Twig template that loads stylesheets and scripts via Assetics and Twig-blocks. Now I added another Bundle that queries data via Buzz from a remote location, which worked fine as a standalone script, but I don't get around printing output in a Twig template.
The architecture of the original script is like the following (names made more generic):
Vendors - abstract class that serves as base for all remote request Bundles.
ServiceABC - abstract class that extends Vendors and defines Error handling and output preparation for the ABC service.
ClientXYZ - final class that extends Service_ABC, defines output parsing and normalization of the returned data.
This Bundle got a services.yml file:
# ~/MyApp/Bundle/ServiceABCBundle/Resources/config/services.yml
parameters:
service_abc_manager.class: MyApp\Bundle\ServiceABCBundle\Models\Service_ABC
location_manager.class: MyApp\Bundle\ServiceABCBundle\Models\Clients\ClientLocation
monitor_manager.class: MyApp\Bundle\ServiceABCBundle\Models\Clients\ClientMonitor
services:
service_abc_manager:
abstract: true
location_manager:
class: %location_manager.class%
parent: service_abc_manager
monitor_manager:
class: %monitor_manager.class%
parent: service_abc_manager
Names changed for easier reference - Typos by accident possible.
Now my problem/question is, that I don't really get behind the Symfony2 concept of how to get the output into the template.
namespace MyApp\Bundle\ServiceABCBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use MyApp\Bundle\ServiceABCBundle\Models\Clients\ClientLocation;
class DefaultController extends Controller
{
public function indexAction()
{
$services = array();
$services[] = $this->container->has('service_abc_manager');
$services[] = $this->container->has('location_manager');
$services[] = $this->container->has('client_location');
$services[] = $this->container->has('ClientLocation');
var_dump( $services );
$client = new ClientLocation();
var_dump( $client );
$response = $this->render(
'Service_ABC:Default:index.html.twig'
);
# $response->setCharset( 'utf-8' );
# $response->headers->set( 'Content-Type', 'text/html' );
return $response;
}
}
The output of the first array() named $services is always false and the $client = new ClientLocation(); throws an Exception that the class name wasn't found.
How can I access those Services/Bundle(parts)/Classes? And how would I render the output to a template?
Update
After I added the complete tree definition to Configuration()->getConfigTreeBuilder(), I'm able to see the definitions in the CLI:
class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root( 'myapp_service_abc' );
$rootNode
->children()
->scalarNode('service_abc_manager')->end()
->scalarNode('location_manager')->end()
->scalarNode('monitor_manager')->end()
->end()
;
return $treeBuilder;
}
}
The CLI command php app/console config:dump-reference myapp_service_abc now gives me the following output:
myapp_service_abc:
service_abc_manager: ~
location_manager: ~
monitor_manager: ~
I can as well see that the config data was loaded, when I var_dump( $loader ); inside MyAppServiceABCExtension right after $loader->load( 'services.yml' ); was called.
The output is the following:
object(Symfony\Component\DependencyInjection\Loader\YamlFileLoader)
protected 'container' =>
object(Symfony\Component\DependencyInjection\ContainerBuilder)
private 'definitions' =>
array
'service_abc_manager' =>
object(Symfony\Component\DependencyInjection\Definition)
'location_manager' =>
object(Symfony\Component\DependencyInjection\DefinitionDecorator)
private 'parent' => string 'service_abc_manager'
// etc.
The problem itself remains: There's still a FALSE return value inside DefaultController()->indexAction() when I var_dump( $this->container->has( 'service_abc_manager' );. I as well tried var_dump( $this->container->has( 'location_manager' ); and var_dump( $this->container->has( 'myapp.service_abc_manager' ); with the same result.

You should not call your services from the twig file, but from the controller.
The role of the controller is to :
validate your forms if there were a form posted
call your services to get some stuffs to display in a view
initialize forms if there is a form to display
return a Response that typically contains a rendered twig view
Do not call your services using something like $client = new ClientLocation();, but call it using the service container. This will allow you to take the whole power of the dependancy injection offered by Symfony2.
Your controller will look like :
<?php
namespace MyApp\Bundle\ServiceABCBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class DefaultController extends Controller
{
public function indexAction()
{
$locationService = $this->container->get('location_manager');
$someStuffs = $locationService->someMethod();
$response = $this->render(
'ServiceABCBundle:Default:index.html.twig', array('stuffs' => $someStuffs)
);
return $response;
}
}
From your twig file, you'll be able to use the stuffs variable :
{{ stuffs }} if your variable is a terminal ( a string, a number... )
{{ stuffs.attribute }} if your variable is an object or an array
About your services file, I am a bit confused, because your architecture does not look to be the standard Symfony2's one :
# ~/MyApp/Bundle/ServiceABCBundle/Resources/config/services.yml
Why your services.yml file isn't in the src/MyApp/SomethingBundle/Resources/config/ directory?
If you didn't already read it, I suggest you to have a look to the Symfony2 : The Big Picture documentation, which is the best way to start with Symfony2.

Related

How can I use the "dump()" twig function in a template that is going to be rendered on production?

I would like to implement a debug output in our test environments in which I would like to output service requests that the application sends.
For this I wanted to use the symfony/twig function dump(), because here the output is wonderfully formatted for all types of variables and also offers the option of opening and closing the structure.
Pseudo-code would be something like this
{% if debugEnabled %}
{{dump (debugInfos)}}
{% endif %}
Unfortunately, "dump" is part of the Symfony DebugBundle, which for good reasons is not loaded in Prod environments and which should stay that way:
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
In the Symfony documentation says:
By design, the dump() function is only available in the dev and test
environments, to avoid leaking sensitive information in production. In
fact, trying to use the dump() function in the prod environment will
result in a PHP error.
I don't want to use dump() in production environments at all, but only locally to output our service requests.
However, I cannot implement a code like above because an error always occurs in production (undefined function dump()) of course, since dump() is not loaded at all.
You can create your own function which tests if the extension is loaded.
As you can see in the source the function twig_var_dump is registered, so you can test if the function exists or not to determine if the debug extension was loaded or not.
Should be fine to register your own function as dump as long as your extension gets loaded after the debug extension
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
use Twig\Environment;
class AppExtension extends AbstractExtension
{
public function getFunctions()
{
return [
new TwigFunction('dump', [$this, 'dump'] , ['needs_context' => true, 'needs_environment' => true, ]),
];
}
public function dump(Environment $env, $context, ...$vars)
{
if (!function_exists('\twig_var_dump')) return;
return \twig_var_dump($env, $context, ...$vars);
}
}
sidenote: Not tested this but should work
Create your own extension that's only loaded in production.
Create a folder (for example Twig) in your src directory.
Make that folder excluded from autowiring if you use the default:
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
- '../src/Twig/' # This is the directory excluded from autowiring
Then create a config/services_prod.yaml file with this content:
services:
App\Twig\FakeDebugExtension: # replace with your class name
tags:
- twig.extension # not needed if you use autoconfiguration
Then create a class with this content:
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
final class FakeDebugExtension extends AbstractExtension
{
public function getFunctions(): array
{
return [
new TwigFunction('dump', [$this, 'fakeDump']),
];
}
public function fakeDump(...$arguments): void
{
}
}

Create custom router service

am creating own framework based on Sf2 commponents and i try to create router service.
I i need that service for generateUrl() method
protected function generateUrl($route, $parameters = array(), $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
return $this->get('router')->generate($route, $parameters, $referenceType);
}
I try this
$container = new ContainerBuilder();
$container->setDefinition('router_loader', new Definition('Symfony\Component\Config\Loader\LoaderInterface'));
$container->setDefinition('router', new Definition('Symfony\Component\Routing\Router', array()));
And when i execute in my methodAction
$this->generateUrl('home');
he return me:
Catchable fatal error: Argument 1 passed to
Symfony\Component\Routing\Router::__construct() must be an instance of
Symfony\Component\Config\Loader\LoaderInterface, none given in
D:\xampp\htdocs\my_fw\vendor\symfony\routing\Router.php on line 95
looking on router constructor i see. I need that interface
public function __construct(LoaderInterface $loader, $resource, array $options = array(), RequestContext $context = null, LoggerInterface $logger = null)
how to avoid that implementation in service?
**New update:** routing.php
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;
// Routing
$routes = new RouteCollection();
// Home
$routes->add('home', new Route('/', array(
'_controller' => 'MyCompany\\Controller\\HomeController::indexAction',
)));
You are having this error because you are configuring the router service with no service definition the service definition should have your service arguments which is the source of your error because the container try to create the router with no arguments check this
For better use you can configure the router service in the service.yml/.xml file
Edit: The documentation page for the Routing component has detailed setup instructions
Try injecting the router_loader service as an argument into the router service. For this case you have to use the Symfony\Component\DependencyInjection\Reference class.
Routes have to be configured with a config file when using the all in one Router class. You have to use a FileLocator and a real Loader class, like the YamlFileLoader, you can't just use the interface (services generally can't be interfaces in Symfony).
The service container setup for the router service should look like this:
use Symfony\Component\DependencyInjection\Reference;
// Loads config files from the current directory, change this to
// your liking, or add more than one path
$container->setDefinition('router_config_locator', new Definition(
'Symfony\Component\Config\FileLocator', [[__DIR__]]
));
$container->setDefinition('router_loader', new Definition(
'Symfony\Component\DependencyInjection\Loader\YamlFileLoader', [
new Reference('router_config_locator'),
]
));
$container->setDefinition('router',
new Definition('Symfony\Component\Routing\Router', array(
'loader' => new Reference('router_loader'),
// Definition of routes in Yaml form
'resource' => 'routes.yml',
))
);
The routes.yml file contains your route definitions:
home:
path: /home
defaults: {_controller: "MyController:index"}
Also have a look at this documentation page about setting up the routing component, which also talks about setting up the routing without the all in one class and config file.

Symfony2 Route prefix querystring dynamic

My symfony2 project is setup with normal YAML routes to any normal project.
Routes are setup with annotation and final URLs are
http://examplecom/artices/{id}
http://example.com/comments/{id}
I want to add prefix querystring to all the path, only if there is querystring called preview
So If I access http://example.com/?preview=something - I want this querystring to append to all the routes, so it continue to pass on every page and if this does not exist, then it will continue to be used as normally.
How can I accomplish this?
service.yml
parameters:
router.options.generator_base_class: "Acme\\DemoBundle\\Routing\\Generator\\UrlGenerator"
UrlGenerator.php
<?php
namespace Acme\DemoBundle\Routing\Generator;
use Symfony\Component\Routing\Generator\UrlGenerator as BaseUrlGenerator;
class UrlGenerator extends BaseUrlGenerator
{
protected function doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens)
{
return parent::doGenerate($variables, $defaults, $requirements, $tokens, $parameters, $name, $referenceType, $hostTokens).'?preview=something';
}
}
reference: http://h4cc.tumblr.com/post/56874277802/generate-external-urls-from-a-symfony2-route

Custom error pages for PROD environment only?

How can I setup custom error pages for PROD environment only? I want to show custom ones for production but ordinary ones with exceptions for dev environment.
Any ideas?
I had the same issue and solution was pretty easy. You have to modify parameter twig.exception_listener.contoller to redirect rendering of error page to your own controller, which may extend original Twig exception controller.
Example (YourBundle/Resources/config/services.xml):
<parameter key="twig.exception_listener.controller">YourBundle\Controller\ExceptionController::showAction</parameter>
Then you have to create your own ExceptionController with method showAction, check for environment and do what you want to do or pass request to parent::showAction().
namespace YourBundle\Controller;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController as BaseExceptionController;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpKernel\Exception\FlattenException;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
use Symfony\Component\HttpFoundation\Response;
class ExceptionController extends BaseExceptionController {
public function showAction(FlattenException $exception, DebugLoggerInterface $logger = null, $format = 'html') {
$kernel = $this->container->get('kernel');
if ($kernel->getEnvironment() == 'prod') {
$request = $this->container->get('request');
$request->setRequestFormat($format);
$templating = $this->container->get('templating');
$code = $exception->getStatusCode();
$template = new TemplateReference('YourBundle', 'Exception', 'errorpage', $format, 'twig');
if ($templating->exists($template)) {
$response = $templating->renderResponse($template, array(
'status_code' => $code,
'message_code' => 'error_' . $code,
'status_text' => Response::$statusTexts[$code],
'requested_url' => $request->getUri(),
));
$response->setStatusCode($code);
$response->headers->replace($exception->getHeaders());
return $response;
}
}
return parent::showAction($exception, $logger, $format);
}
}
Beware of errors in errorpage.html.twig, because exceptions in twig processing are not handled as usual.
If you don't want to override the exception controller :
You can first copy the entire folder (or specific layout file) at
\vendor\symfony\symfony\src\Symfony\Bundle\TwigBundle\Resources\views\
to
\app\Resources\TwigBundle\views
Then customise the view in each layout file to match your design.
Then in the layout file, customise the message for each environment as follow
{% if app.environment == 'prod' %}
// message for prod
{% else %}
// message for dev
{% endif %}

How do I read configuration settings from Symfony2 config.yml?

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

Resources