Symfony : clear cache and warmup when users are using the website - php

With Symfony, when config or twig files are modified, the cache must be cleared and a warmup must be performed to take into account the new values.
My problem is when users are working on the website and I would like to update a file which needs a Symfony warmup command : the command fails if a user is consulting the cache at the same times by browsing the website. Then the cache is corrupted and I need to run again the clear cache and the warmup command when users are angry because the website is not working and hit the F5 button again and again making this process endless...
To avoid this, I am always planning a maintenance and block website accessibility during the cache warmup.
But, it is a complex task to simply fix a typo, isn't it?
Is there a way to clear and warmup single file? Or any idea to handle this process correctly?

Works for me, might work for you.
I usually have two versions of my app placed side by side. Only one is connected to web server. If I have to make any changes I update inactive version, clear cache, warmup cache, etc. Then I switch active version in web server.
That way you have as much time as you want for maintenance and switch is unnoticeable for your users.
You can also configure web server to allow inactive version to be available in some internal channel. That way, after you done what you wanted to change you can peek if everything works as expected(or let testers do their work) before you go public.

At first you have to register a EventListener:
# /config/services.yaml
parameters:
lockFilePath: "%kernel.root_dir%/../web/.lock"
services:
maintenance_listener:
class: App\EventListener\MaintenanceListener
arguments:
- "%lockFilePath%"
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
Then the EventListener itself:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class MaintenanceListener
{
private $lockFilePath;
public function __construct($lockFilePath)
{
$this->lockFilePath = $lockFilePath;
}
public function onKernelRequest(GetResponseEvent $event)
{
if ( ! file_exists($this->lockFilePath)) {
return;
}
$event->setResponse(
new Response(
'site is in maintenance mode',
Response::HTTP_SERVICE_UNAVAILABLE
)
);
$event->stopPropagation();
}
}
So if you now create a .lock file in /web then your site is in maintenance mode.
If you want to display a special template, you can inject #templating into the EventListener.

Related

Access cache items created outside Symfony makes them work sometimes only, why?

I am integrating a legacy Zend Framework 1 (ZF1) application and a Symfony 3.2.6 (SF) application. In a nutshell how it works is:
The session management, the login page (unique entrypoint) and a lot of stuff are managed by the ZF1 application and ZF1 itself
There is not call to any Zend controller, templates, helpers or any other from SF side
As an example:
- http://localhost/login => will be managed by ZF1
- http://localhost/sf/quote => will be managed by SF (the key is the /sf/ in the URL)
This mean I have a rule in the Apache VH saying: each request with /sf/* on the URL sent it to app.php|app_dev.php which is Symfony otherwise it'll bypass this rule and it'll go to ZF1 directly.
Having that first thing I do is login in the application using legacy ZF1 application. After login successfully I redirect to the dashboard an a NavigationController.php is invoked from the main.phtml layout using the following code: $this->action('buildallmenu', 'navigation');.
In such code the menu gets generated from DB and then using the code below I am trying to cache it since I don't need to and I don't want to access the DB once again from ZF1 nor from SF.
use Predis\Client;
use Symfony\Component\Cache\Adapter\RedisAdapter;
$cached_items = [
'main_nav' => $main_nav,
'sub_nav' => $sub_nav,
'footer_nav' => $footer_nav,
'view_as' => $view_as,
];
$redisConnection = new Client('tcp://cache_server:6379');
$cache = new RedisAdapter($redisConnection);
$menu = $cache->getItem('mmi_menus_'.session_id());
if (!$menu->isHit()) {
$menu->set($cached_items);
$cache->save($menu);
}
return $menu->get();
Why session_id() because the menu is "unique" per user so makes sense to append the session_id() to the Redis cache item.
From there I am seeing the $cached_items var populated with the proper content and it's saved to the Redis.
Now the way I access to a Symfony controller is how I explained before: "by accessing a URL". Let's say I called a URL as: http://localhost/sf/quote this will execute the rule and redirect to app_dev.php which means I am on Symfony now.
First thing I did was check the session_id() (printing the session_id() value) and compare against the value created by ZF1 and they match.
The SF base template call a controller as: {{ render(controller('CommonBundle:Setup:GenerateMenuItems')) }}. This is the content of the function called from the template:
public function GenerateMenuItemsAction()
{
$menu = $this->get('mmi_pool')->getItem('mmi_menus_'.session_id());
dump('mmi_menus_'.session_id());
if ($menu->isHit()) {
return $this->render(
'CommonBundle:Layout:menu.html.twig',
['menu' => $menu->get()]
);
}
return new Response();
}
mmi_pool is a service which defintion is as follow:
mmi_pool:
parent: cache.adapter.redis
tags:
- name: cache.pool
namespace: ''
How cache is configured at config.yml?
framework:
cache:
default_redis_provider: redis://%redis_host%:%redis_port%
Update
I have found that when I login for first time this piece of code is not executed:
if (!$menu->isHit()) {
$menu->set($cached_items);
$cache->save($menu);
}
I am not sure the why. That's causing the cache to store the wrong items and therefore show the wrong items on SF.
what I am doing wrong here? I know caching is tricky but certainly I am missing something here
First time $menu is null, but it always null because of that code: if (!$menu->isHit()) {
return ... You exit controller with return statement.
if (!$menu->isHit()) {
$menu->set($cached_items);
$cache->save($menu);
}
return $menu->get();

Symfony, dynamic routing

I have a symfony project with multiple skins/templates that have their own routes, does anyone have an idea for a correct setup?
Every skin/template is its own bundle, since its not just skins and assets, but maybe also services that might exist in some skins.
Hostname decides the skin.
Using a custom RouteLoader to load the route.yml of the target bundle.
The custom RouteLoader does the job--but the generated routes are getting cached, and as far as i understand, there is no way to prevent route caching.
Some suggestions are:
Creating a /{dynamic} route, so manually form routes.. But i dont want to throw away that piece of functionality of the router, or refactor the entire project..
Prefix the routes with the template identifier. This would require me to load all route.yml files, which isnt possible since their share paths.
Anyone? I cant go with multiple projects really, the amount of skins will be around 20-30~.
The reason for this setup is because its a target of Content-as-a-Service .. service, multiple clients use the project as a platform, and their setting decides which templates gets used.
It sounds like you want to dynamically load bundles based on the host name? Not going to happen with Symfony 2 because of the caching. Especially the services.
Your best bet is to setup an app for each skin and then do some url majic to execute the desired app.php file. Clearly since you have defined a bundle for each skin then there is a finite number so having multiple apps should not be much or a burden.
It's possible that you might be able to work around the template issue. You would still need to load all your skin bundles but you could futz around with the template names or paths and probably get something to work.
But services? Unless you start appending host names to service id's then I don't see any work around.
I think it's possible to load dynamically twig templates depending of your user by adding a listener on kernel requests.
I can give you a piece of code which, I hope, could help you :
/**
* On Kernel Request triggers the request to get the user config
* then adds TWIG paths depending on user TemplateName
*/
public function onKernelRequest(GetResponseEvent $event)
{
if (HttpKernelInterface::MASTER_REQUEST !== $event->getRequestType()) {
return;
}
//$userConfig = Retrieve your user config
if (null === $userConfig->getTemplateConfig()->getTemplate()->getName())
{
throw new TemplateConfigNotFoundException(sprintf("Could not find TemplateConfig for %s", $userConfig->getName()));
}
$template = $userConfig->getTemplateConfig()->getTemplate()->getName();
$path = sprintf('%s/../../%s/Resources/views', __DIR__, ucfirst($template));
if (!is_dir($path)) {
throw new TemplateNotFoundException(sprintf("Could not find template %s", $template));
}
$this->loader->prependPath($path);
$this->loader->addPath(sprintf('%s/../Resources/views/Default', __DIR__));
}
With $this->loader defined as \Twig_Loader_Filesystem in your Listener constructor
Hope it can give you a clue
Symfony2 already supports host aware routing out-of-the-box, like this:
website_customer_1:
path: /
host: customer1.example.com
defaults: { _controller: Customer1Bundle:Main:startPage, theme: template1 }
website_customer_2:
path: /
host: customer2.example.com
defaults: { _controller: Customer1Bundle:Main:startPage, theme: template2 }

How do I change the URL Alias for Security/login in SilverStripe to user/login

I am working on a new website being built in SilverStripe. Currently I am having a ton of trouble trying to get the system to let me change the URL alias (or create a second one) for the Security controller's login (and eventually logout) function.
I have tried playing around with the routes.yml file and I tried creating the paths in my own UserController and loading directly from the Security controller with "return Security::login()". But that gives me errors about the use of the static functions.
Unfortunately I don't come from a ton of object oriented experience and this is the first CMS I have used that actually uses a bunch of true object orientation. The current version of SilverStripe we are using is 3.0 (but we will be upgrading to 3.1.1 in a few days).
Does anyone know much about the routing in SilverStripe?
as you stated correctly, SilverStripe has routes, and they can be defined in a yaml config file.
if you take a look at the existing routes.yml in the framework you can see how the default security route is defined:
https://github.com/silverstripe/silverstripe-framework/blob/fd6a1619cb7696d0f7e3ab344bc5ac7d9f6cfe77/_config/routes.yml#L17
if you just want to replace the Secuirty in Secuirty/login, its as easy as just creating your own routes.ymlin mysite/_config/ with the following content:
---
Name: myroutesorsomeotherrandomname
Before: '*'
After:
- '#rootroutes'
- '#coreroutes'
- '#modelascontrollerroutes'
- '#adminroutes'
---
Director:
rules:
'foobar//$Action/$ID/$OtherID': 'Security'
NOTE: make sure you ran a ?flush=1 to ensure the yml file is loaded (they get cached)
also make sure you use spaces in the yml file, if you use tabs you are going to have a bad time
if you however wish to also replace /login and /logout this is no longer a thing for routing.
login and logout are actions (php functions that are listed in Security::$allowed_actions and thus available as URL) on the on Security.
but its still rather easy, just subclass Security and create the actions you want:
<?php
class MySuperSecurity extends Security {
private static $allowed_actions = array(
'showMeTheLoginForm',
'alternative_login_action_with_dashes',
'doTheLogout',
);
function showMeTheLoginForm() {
// can be visited via http://website.com/foobar/showMeTheLoginForm
return $this->login();
}
function alternative_login_action_with_dashes() {
// can be visited via http://website.com/foobar/alternative-login-action-with-dashes
return $this->login();
}
function doTheLogout($redirect = true) {
// can be visited via http://website.com/foobar/doTheLogout
return $this->logout($redirect);
}
}
and make the route point to your new class instead of Security inside the routes.yml:
'foobar//$Action/$ID/$OtherID': 'MySuperSecurity'
NOTE: again, make sure you did a ?flush=1, both the private static $allowed_actions as well as the yml config file are cached by silverstripe.
NOTE: both solutions suggested in this post will create an additional route to login and does not replace the existing one, so the old Security/login will still be available
I don't know nothing about SilverStripe excepting that is a CMS, but i think SilverStripe must provide a way to aliases Url. Also an alternative is create Alias in virtual host definition if you're using apache or in .htaccess file. Refer to apache doc to further details. If you post a skeleton of your .htaccess file or VirtualHost definition i could help you.

Is there a way to force Yii to reload module assets on every request?

My website is divided into separate modules. Every module has it's own specific css or js files in /protected/modules/my_module/assets/css or js for js files. Yiis assets manager creates folder when I first use page that uses my assets.
Unfortunately if I change sth in my files - Yii does not reload my css or js file. I have to manually delete /projects/assets folder. It is really annoying when you are developing the app.
Is there a way to force Yii to reload assets every request?
In components/Controller.php add the following (or adjust an existing beforeAction):
protected function beforeAction($action){
if(defined('YII_DEBUG') && YII_DEBUG){
Yii::app()->assetManager->forceCopy = true;
}
return parent::beforeAction($action);
}
What this does it that before any actions are started, the application will check to see if you are in debug mode, and if so, will set the asset manager to forcibly recopy all the assets on every page load.
See: http://www.yiiframework.com/doc/api/1.1/CAssetManager#forceCopy-detail
I have not tested this, but based on the documentation I believe it should work fine.
Note: The placement of this code within beforeAction is just an example of where to put it. You simply need to set the forceCopy property to true before any calls to publish(), and placing it in beforeAction should accomplish that goal.
If you're using Yii2 there is a much simpler solution through configuration.
Add the following to your 'config/web.php':
if (YII_ENV_DEV) {
// configuration adjustments for 'dev' environment
// ...
$config['components']['assetManager']['forceCopy'] = true;
}
This forces the AssetManager to copy all folders on each run.
An alternatively solution is to publish your module assets like this:
Yii::app()->assetManager->publish($path, false, -1, YII_DEBUG);
The fourth parameter enforces a copy of your assets, even if they where already published.
See the manual on publish() for details.
Re-publishing assets on every request potentially takes a lot of resources and is unnessecary for development.
For development, it's much easier to use the linkAssets feature of
CClientScript. Assets are published as symbolic link directories, and
never have to be regenerated. See:
http://www.yiiframework.com/doc/api/1.1/CAssetManager#linkAssets-detail
For staging/production, you should make clearing the assets/ folder
part of your update routine/script.
Only fall back to one of the other solutions if for some reason you cannot use symbolic links on your development machine (not very likely).
In YII 1 in config we have:
'components'=> [
...
'assetManager' => array(
'forceCopy' => YII_DEBUG,
...
)
...
]

Changing locale with symfony 2.1

Having some issue changing the locale on a symfony 2.1 website.
I can't find a way to be able to change the lang without using the _locale on every routes. I know this is against the fundamental rule, but this content will for example not be indexed by engine as it is member only.
Typically, I would like a simple method to be able to change the locale on the request (BC from version 2.1), or on the session, but can't figure out how to do that smoothly. I also would like to avoid the use of a Listener for that.
config.yml file :
framework:
translator: { fallback: %locale% }
session:
routing.yml file :
route_change_lang:
pattern: /changelang/{newlang}
defaults: { _controller: AcmeCoreBundle:Default:switchLanguage, newlang: en }
requirements:
newlang: en|fr|de
Simple action to update the locale of the router :
public function switchLanguageAction($newlang)
{
$request = $this->getRequest();
$request->setLocale($newlang);
$referer_url = $this->get('request')->headers->get('referer');
if ($referer_url != null) {
return $this->redirect($referer_url);
} else {
return $this->redirect($this->generateUrl('route_home'));
}
}
What is the problem? I guess it is related to the default_locale set in the main config.yml file, but documentation is not really clear, any help/hint appreciated
I've come across the same problem, since we cant' use locales in our urls (seo-issues). Also we use locales like en_US and those are stored in a config outside the direct framework access.
What I did is registering an event listener and hooking into the onKernelRequest event. There I check if locale is set in session, if not, I add it to both, request and session.
This way, the framework keeps on behaving like it did before 2.1
If you need more info on how to do this, comment and I'll edit some exaples in here :-)
Restore the old behavior as explain in https://github.com/symfony/symfony/blob/master/UPGRADE-2.1.md#httpfoundation-1
And use the piece of code of Carlos Granados.
You can also read my another answer https://stackoverflow.com/a/12952999/520114
If you set the locale in the request, this is just used for the current request. The next time that a request is issued, the default_locale will be used. Even if now (2.1) the locale is set in the request instead of the session, "It is also possible to store the locale in the session instead of on a per request basis. If you do this, each subsequent request will have this locale." (from the docs). So, you need to do:
$this->get('session')->set('_locale', $newlang);

Categories