Symfony2 dynamic routing - caching issue - caching

I'm trying to create dynamic routes as I have created a CMS where each page created can be associated to a route. I'm using the example from this link - http://php-and-symfony.matthiasnoback.nl/2012/01/symfony2-dynamically-add-routes/ and all works fine, however the routing is cached, therefore one route will work but then the next won't unless I clear the cache. Is it possible to remove just the routing cache at this stage or is there another alternative? I don't want to remove the whole cache directory on each page load as that wouldn't make sense. Here is the example code:
namespace Acme\RoutingBundle\Routing;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
class ExtraLoader implements LoaderInterface
{
private $loaded = false;
public function load($resource, $type = null)
{
if (true === $this->loaded) {
throw new \RuntimeException('Do not add this loader twice');
}
$routes = new RouteCollection();
$pattern = '/extra';
$defaults = array(
'_controller' => 'AcmeRoutingBundle:Demo:extraRoute',
);
$route = new Route($pattern, $defaults);
$routes->add('extraRoute', $route);
return $routes;
}
public function supports($resource, $type = null)
{
return 'extra' === $type;
}
public function getResolver()
{
}
public function setResolver(LoaderResolver $resolver)
{
// irrelevant to us, since we don't need a resolver
}
}
Then I've made a service for the ExtraLoader:
<!-- in /src/Acme/RoutingBundle/Resources/config/services.xml -->
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="acme.routing_loader" class="Acme\RoutingBundle\Routing\ExtraLoader">
<tag name="routing.loader"></tag>
</service>
</services>
</container>
The last thing we need, is a few extra lines in /app/config/routing.yml:
AcmeRoutingBundle:
resource: .
type: extra

This is quite inefficient, because with each new route you have to clear cache, so you'll be bound by hdd/ssd with useless clear cache.
The alternative is to create a new method in controller which accepts a dynamic page on GET and to show the dynamic content in twig.
You can create a service to render the dynamic pages, which will simplify things.

Do you have looked at the DynamicRouter from the symfony-cmf project? I think this fits your needs and is exactly created for your use case.
You current implementation has some really issues you should know about. First of all, you have to clear the routing cache, for each route you create/edit/delete. This leads to race conditions and memory peaks for no reason.
The default implementation from symfony is to handel static routes, not dynamic ones.

I researched and tried out a bit and I found out that you can just delete the following files:
for dev:
/app/cache/dev/appDevUrlGenerator.php
/app/cache/dev/appDevUrlGenerator.php.meta
/app/cache/dev/appDevUrlMatcher.php
/app/cache/dev/appDevUrlMatcher.php.meta
for prod:
/app/cache/prod/appProdUrlGenerator.php
/app/cache/prod/appProdUrlMatcher.php
There is only one minior downside of this. I am using the current route to determine if a menu item is active or not:
{% set currentPath = path(app.request.attributes.get('_route'), app.request.attributes.get('_route_params')) %}
...
<li{% if currentPath == path('mybundle_default_index') %} class="active"{% endif %}>
In this case app.request.attributes.get('_route') is still cached as a route that might not exist anymore. I don't know yet if this only concerns the twig cache or other parts too.
Also I don't understand why you would have to delete the whole cache on each page load? You only have to clear the cache when new routes are added.

I've resolved this problem in my own CMS.
At first I overrode the base Router class:
parameters:
router.class: Name\Of\Your\Router
and extended it:
use Symfony\Bundle\FrameworkBundle\Routing\Router as BaseRouter;
class Router extends BaseRouter
{
public function clearCache($cache_dir, $environment, $warm_up)
{
$cache_dir .= '/'. $environment;
$environment = ucfirst($environment);
#unlink($cache_dir .'/app'. $environment .'UrlMatcher.php');
#unlink($cache_dir .'/app'. $environment .'UrlGenerator.php');
if ($warm_up) {
$this->matcher = null;
$this->generator = null;
$this->warmUp($cache_dir);
}
}
}
Secondly I created a service CacheService:
cache_service:
class: Name\Of\Your\CacheService
arguments:
- #router
- %kernel.environment%
- %kernel.root_dir%/cache
and added the following method:
public function clearCache($environment = null)
{
if (null === $environment) {
$environment = $this->environment;
}
$this->router->clearCache($this->cache_dir, $environment, $this->environment == $environment);
}
So now I can call this method when I need to clear cache for current or specific environment.

Related

Unable to render HTML from Markdown

I am going through an online course on Laravel. This course is using the League\commonmark package for converting markdown to html.
Whenever the package is used in the app, I get:
Unable to find corresponding renderer for block type League\CommonMark\Block\Element\Document
The app uses the following presenter to do the conversion.
class PagePresenter extends AbstractPresenter
{
protected $markdown;
public function __construct($object, CommonMarkConverter $markdown)
{
$this->markdown = $markdown;
parent::__construct($object);
}
public function contentHtml()
{
return $this->markdown->convertToHtml($this->content);
}
}
Can anyone point me in the right direction?
That happens because the IoC is resolving the dependencies for CommonMarkConverter, specifically Environment which is instantiated with all null properties.
You can probably resolve this by using a Laravel specific integration: https://github.com/GrahamCampbell/Laravel-Markdown
Or you can bind and instance to the service container this way:
In your AppServiceProvider, register method add this:
$this->app->singleton('Markdown', function ($app) {
// Obtain a pre-configured Environment with all the CommonMark parsers/renderers ready-to-go
$environment = \League\CommonMark\Environment::createCommonMarkEnvironment();
// Define your configuration:
$config = ['html_input' => 'escape'];
// Create the converter
return new \League\CommonMark\CommonMarkConverter($config, $environment);
});
Now remove CommonMarkConverter from your Presenter constructor add use app('Markdown'):
class PagePresenter extends AbstractPresenter {
protected $markdown;
public function __construct($object)
{
$this->markdown = app('Markdown');
parent::__construct($object);
}
public function contentHtml()
{
return $this->markdown->convertToHtml($this->content);
}
}
You just put a line in the config/app.php file
'Markdown' => GrahamCampbell\Markdown\Facades\Markdown::class,

Angular2: unable to navigate to url using location.go(url)

I am trying to navigate to a specific url using location.go service from typescript function. It changes the url in the browser but the component of the url is not reflected in the screen. It stays on the login (actual) screen - say for eg:
constructor(location: Location, public _userdetails: userdetails){
this.location = location;
}
login(){
if (this.username && this.password){
this._userdetails.username = this.username;
this.location.go('/home');
}
else{
console.log('Cannot be blank');
}
}
Is there a compile method or a refresh method I am missing?
Yes, for redirecting from one route to another you should not be using location.go, it generally used to get normalized url on browser.
Location docs has strictly mentioned below note.
Note: it's better to use Router service to trigger route changes. Use
Location only if you need to interact with or create normalized URLs
outside of routing.
Rather you should use Router API's navigate method, which will take ['routeName'] as you do it while creating href using [routerLink] directive.
If you wanted redirect via URL only then you could use navigateByUrl which takes URL as string like router.navigateByUrl(url)
import {
ROUTER_DIRECTIVES,
RouteConfig,
ROUTER_PROVIDERS,
Location, //note: in newer angular2 versions Location has been moved from router to common package
Router
} from 'angular2/router';
constructor(location: Location,
public _userdetails: userdetails,
public _router: Router){
this.location = location;
}
login(){
if (this.username && this.password){
this._userdetails.username = this.username;
//I assumed your `/home` route name is `Home`
this._router.navigate(['Home']); //this will navigate to Home state.
//below way is to navigate by URL
//this.router.navigateByUrl('/home')
}
else{
console.log('Cannot be blank');
}
}
Update
In further study, I found that, when you call navigate, basically it does accept routeName inside array, and if parameters are there then should get pass with it like ['routeName', {id: 1, name: 'Test'}].
Below is API of navigate function of Router.
Router.prototype.navigate = function(linkParams) {
var instruction = this.generate(linkParams); //get instruction by look up RouteRegistry
return this.navigateByInstruction(instruction, false);
};
When linkParams passed to generate function, it does return out all the Instruction's required to navigate on other compoent. Here Instruction means ComponentInstruction's which have all information about Routing, basically what ever we register inside #RouteConfig, will get added to RouteRegistry(like on what path which Component should assign to to router-outlet).
Now retrieved Instruction's gets passed to navigateByInstruction method, which is responsible to load Component and will make various thing available to component like urlParams & parent & child component instruction, if they are there.
Instruction (in console)
auxInstruction: Object //<- parent instructions
child: null //<- child instructions
component: ComponentInstruction //<-current component object
specificity: 10000
urlParams: Array[0] <-- parameter passed for the route
urlPath: "about" //<-- current url path
Note: Whole answer is based on older router version, when Angular 2 was in beta release.

Laravel 4 Container Internal Workings

I've been studying the laravel 4 container to get more knowledge of the internals of laravel and to upgrade my own skills in writing better code.
However i'm failing to understand 3 similar pieces of code.
I'll use the smallest snippet to keep this question clean.
Similar questions can be found in links below. Although people have replied with correct answers, I'm not satisfied with simply 'Knowing how to use it, but not knowing how it all works inside'. So i really hope someone can give an explanation to all this.
Question 1
Question 2
<?php namespace Illuminate\Container; use Closure, ArrayAccess, ReflectionParameter;
class BindingResolutionException extends \Exception {}
class Container implements ArrayAccess {
/**
* Wrap a Closure such that it is shared.
*
* #param Closure $closure
* #return Closure
*/
public function share(Closure $closure)
{
return function($container) use ($closure)
{
// We'll simply declare a static variable within the Closures and if
// it has not been set we'll execute the given Closure to resolve
// the value and return it back to the consumers of the method.
static $object;
if (is_null($object))
{
$object = $closure($container);
}
return $object;
};
}
}
How does the share method know that the $container variable in that function is in fact an instance of Illuminate\Container? It isn't defined within the scope of that function.
Neither is it defined in the following example usecase (which wouldn't help anyway)
class AuthServiceProvider extends ServiceProvider{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->app['auth'] = $this->app->share(function($app)
{
// Once the authentication service has actually been requested by the developer
// we will set a variable in the application indicating such. This helps us
// know that we need to set any queued cookies in the after event later.
$app['auth.loaded'] = true;
return new AuthManager($app);
});
}
}
I'd expect a different implementation, so here comes
class MyContainer{
public function share(Closure $closure)
{
$container = $this;
return function() use ($closure, $container)
{
static $object;
if(is_null($object))
{
$object = $closure($container);
}
return $object;
};
}
}
$closure = function($container)
{
var_dump($container);
};
$container = new MyContainer();
call_user_func($container->share($closure));
//dumps an instance of MyContainer -> which is the wanted behaviour
$container = new Illuminate\Container\Container();
call_user_func($container->share($closure));
//Throws a warning AND a notice
//Warning: Missing argument 1 for Illuminate\Container\Container::Illuminate\Container\{closure}() in /Users/thomas/Sites/Troll/vendor/illuminate/container/Illuminate/Container/Container.php on line 128
//NOTICE: Notice: Undefined variable: container in /Users/thomas/Sites/Troll/vendor/illuminate/container/Illuminate/Container/Container.php on line 137
//and even worse the output of the var_dump is NULL
I have the same problem in understanding the extend and the bind method, which both have the same implementation of passing a none-existing parameter as a closure argument, but i cannot grasp how it is resolved to the container instance itself?
The return value of Container::share() is a function that takes one argument: the container itself. In order to call it externally, you'd have to do this:
$closure = function ($container) {
var_dump($container);
};
$container = new Illuminate\Container\Container();
call_user_func($container->share($closure), $container);
The reason for this is due to how service definitions work. The intended use of share is to wrap around a service definition.
Like this:
$container = new Illuminate\Container\Container();
$container['foo'] = $container->share(function ($container) { return new Foo(); });
When you access a service, like this:
var_dump($container['foo']);
It checks if the value is callable, and if it is, it will try to call it as a function. If you leave off the share, you will get a new Foo instance every time. The share memoizes the instance and returns the same one every time.
To re-iterate, the $container argument in the function returned from share is there because that's how service creation works. The service definition ("factory" function that you "set" on the container) is just a function that takes a container and returns the instance of the service it is creating.
Since offsetGet() it is expecting the definition to take a $container argument, that's what share returns.

Symfony2 Properly Hook in Events from Services

I have a class which is used to generate navigation from a variety of interconnected bundles. I have a Navigation service to accomplish this.
In order to connect this service with the other bits of Navigation, I want to allow the other bundles to define their own services which then listen to the event listener and add their navigation items at the proper time.
The problem is, I can't figure out how to have a service listen to an event without first calling that service manually in order to create it.
Any ideas?
To give a more concrete idea, I have something like this:
// Set up as a service in the bundle.
class Navigation {
// ...
protected $dispatcher; // event dispatcher passed in to service
// ...
public function generateNavigation() {
$items = array();
// add some items
$event = new NavigationEvent($items); // custom event
$this->eventDispatcher->dispatchEvent('navigation_event', $event);
}
}
// Set up as a service in some secondary bundle.
class NavigationWorker {
/**
* #param $dispatcher Same instance as Navigation
*/
public function __construct(EventDispatcher $dispatcher) {
$dispatcher->addListener('navigation_event', array($this, 'doSomething'));
}
}
With this set up, it should work if the NavigationWorker is called at some point and is constructed, but I can't always call them directly, so it is never constructed and the listener is never added.
The way I currently do it is to pass all of the NavigationWorkers to Navigation and have it add their listener, but this is very ugly.
See the Event Listener Documentation. Make NavigationWorker and event listener and it won't need to be explicitly constructed.
I'm changing the answer to this because while that set me on the right path, it wasn't the complete answer. That article really only allows you to hook in to pre-defined kernel events. I however needed my own, so I started working back from there.
In the end, I ended up creating my own tags, a compiler pass to process those tasks. I also added my own extension of EventDispatcher, though that wasn't super-necessary (you could just use the normal one).
Here is what the file solution looked like.
Configuration:
parameters:
my_bundle.navigation.event.class: My\Bundle\DependencyInjection\NavigationEvent
my_bundle.event_dispatcher.class: My\Bundle\DependencyInjection\EventDispatcher
my_bundle.navigation.class: My\Bundle\DependencyInjection\NavigationGenerator
my_bundle.navigation_listener1.class: My\Bundle\DependencyInjection\NavigationListener
my_bundle.navigation_listener2.class: My\Bundle\DependencyInjection\NavigationListener
services:
my_bundle.event_dispatcher:
class: %my_bundle.event_dispatcher.class%
my_bundle.navigation:
class: %my_bundle.navigation.class%
arguments:
- #my_bundle.event_dispatcher
my_bundle.navigation_listener1.class:
class: %my_bundle.navigation_listener1.class%
tags:
- { name: my_bundle.event_listener, event: my_bundle.navigation.generate, method: onGenerateNavigation }
my_bundle.navigation_listener2.class:
class: %my_bundle.navigation_listener2.class%
tags:
- { name: my_bundle.event_listener, event: my_bundle.navigation.generate, method: onGenerateNavigation }
CompilerPass:
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class EventListenerCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('my_bundle.event_dispatcher')) {
return;
}
$definition = $container->getDefinition(
'my_bundle.event_dispatcher'
);
$taggedServices = $container->findTaggedServiceIds(
'my_bundle.event_listener'
);
foreach ($taggedServices as $id => $tagAttributes) {
foreach ($tagAttributes as $attributes) {
$definition->addMethodCall(
'addListener',
array($this->getEventString($attributes['event'], $container), array(new Reference($id), $attributes['method']))
);
}
}
}
protected function getEventString($str, ContainerBuilder $container)
{
preg_match('/(.*)\.([^.]*)$/', $str, $matches);
$parameterName = $matches[1];
$constName = strtoupper($matches[2]);
$eventClass = $container->getParameter($parameterName . '.event.class');
if (!$eventClass) {
throw new Exception('Unable to find parameter: ' . $eventClass . '.event.class');
}
// Return the value of the constant.
return constant($eventClass . '::' . $constName);
}
Add a function like this to your compiler class (something like MyBundleBundle).
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new EventListenerCompilerPass());
}
Now the EventListener will have added listeners for each of those events. You than just implement everything else exactly as you would expect (Navigation dispatches events which it listens too). You can than hook in new event listeners from any bundle, and they don't even need to share a common class/interface.
This also works for any custom event, as long as the object which has the constant for the event is registered in the parameters with ".event.class" at the end (so my_bundle.navigation.generate looks for the parameter my_bundle.navigation.event.class, uses that class and the constant GENERATE).
Hopefully that'll help anyone else looking to do something similar.

Using only a controller in FW1 without a view

I have an Ajax request that sends some data to a page and expects back a truthy or falsey value depending on if the data was saved. In my controller I do everything and set the content to a true or false value. I really don't want to create a view just to output 1 variable, so I was wondering if there was a way that I don't have to use a view and only use the controller to output simple strings.
I believe you cannot disable views completely, but there's a pretty simple workaround: you can create one view and use it for many actions.
Let's say we've created the view views/main/ajax.cfm, what could be inside it? Obviously, simplest way is:
<cfoutput>#HTMLEditFormat(rc.response)#</cfoutput>
Personally I like returning JSON, it allows me to have status field, plus data, if needed. This way my view looks like this:
<cfheader name="Content-Type" value="application/json" />
<cfoutput>#SerializeJSON(rc.response)#</cfoutput>
Any way, now in our action we need to do something like this:
// prevent displaying the layout
request.layout = false;
// force special view
variables.fw.setView("main.ajax");
// init response (according to the choice made earlier)
rc.response["status"] = "OK";
rc.response = "";
There's one more gotcha for this. Sometimes you don't want AJAX page to be accessed directly (like opened in browser), or vise-versa -- want to do some debugging when it is.
There's a cool helper isAjax in CFWheels framework, it is easy to port to the FW/1. It could be as simple as adding method like this to controller:
/*
* Check if request is performed via AJAX
*/
private boolean function isAjax() {
return (cgi.HTTP_X_REQUESTED_WITH EQ "XMLHTTPRequest");
}
Actually, that setup code above is also helper method in my apps:
/*
* Set up for AJAX response
*/
private struct function setAjax() {
// prevent displaying the layout
request.layout = false;
// force special view
variables.fw.setView("main.ajax");
local.response["status"] = "OK";
return local.response;
}
So in my action code whole check looks like this, which is pretty compact and convenient:
if (isAjax()) {
rc.response = setAjax();
}
else {
return showNotFound();
}
Hope this helps.
You can't output directly from a Controller: its job is just to call the Model and pass data to the View, so you'll need a view template to do the outputting.
However, you can avoid having to create a separate view for each controller method by using the framework's setView() method. This allows you to override the convention and apply a single view to multiple controller methods. So you could set up a generic "ajax view" and then use it to output the data from any of your controllers:
views/main/ajax.cfm
<!---Prevent any layouts from being applied--->
<cfset request.layout=false>
<!--- Minimise white space by resetting the output buffer and only returning the following cfoutput --->
<cfcontent type="text/html; charset=utf-8" reset="yes"><cfoutput>#rc.result#</cfoutput>
controller.cfc
function init( fw )
{
variables.fw=arguments.fw;
return this;
}
function getAjaxResponse( rc )
{
rc.result=1;
fw.setView( "main.ajax" );
}
function getAnotherAjaxResponse( rc )
{
rc.result=0;
fw.setView( "main.ajax" );
}
You can use onMissingView in you Application.cfc to handle the response for ajax calls, this way you don't need to perform any extra logic in your controller methods.
// Application.cfc
function onMissingView(rc) {
if(structKeyExists(rc, "ajaxdata") && isAjaxRequest()) {
request.layout = false;
content type="application/json";
return serializeJSON(rc.ajaxdata);
}
else {
return view("main/notfound");
}
}
function isAjaxRequest() {
var headers = getHttpRequestData().headers;
return structKeyExists(headers, "X-Requested-With")
&& (headers["X-Requested-With"] eq "XMLHttpRequest");
}
// controller cfc
function dosomething(rc) {
rc.ajaxdata = getSomeService().doSomething();
}
This checks if the request context has an ajaxdata key, and is a genuine ajax request, then returns the serialize data. If it doesn't then it renders the main.notfound view

Resources