CakePHP: CacheHelper and themed views - php

In a CakePHP (2.1) app I'm using themes together with cacheAction. When accessing the view of e.g. /controller/action, its cache file is saved to tmp/views/controller_action.php. When accessing the same view from a mobile url (like m.example.com), I internally use the same app with themed views to simplify the output for the mobile device.
But now there is a problem with the cache: both requests have different hostnames and themes, but the same controller/action and therefore the same filename of the cache files. So when accessing the urls with different hosts or themes, CakePHP returns the same cache file (or more precisely the cache file of the first request). It is not possible to change the name of the cache files depending on a parameter (e.g. hostname or theme).
I tried modifying the parameters of CacheHelper, but without success. Is there a way to change the cache path/prefix for CacheHelper on the fly? Or is there another possibility to realize this behavior?

The only workaround to solve this problem are the following steps:
1) Create an own MyCacheHelper that extends CacheHelper and save it to app/View/Helper/CacheHelper.php. Overwrite the method _writeFile() and extend the row of the $path string with your prefix:
App::uses('Helper', 'Cache');
class MyCacheHelper extends CacheHelper
{
public function _writeFile($content, $timestamp, $useCallbacks = false)
{
// ...
$cache = $prefix.strtolower(Inflector::slug($path));
// ...
}
}
2) Create an own MyDispatcher that extends Dispatcher and save it to app/Lib/Routing/MyDispatcher.php. Overwrite the method cached() and extend the row of the $path string with your prefix:
App::uses('Dispatcher', 'Routing');
class MyDispatcher extends Dispatcher
{
public function cached($path)
{
// ...
$path = $prefix.strtolower(Inflector::slug($path));
// ...
}
}
3) Change the file app/webroot/index.php to use your new dispatcher:
App::uses('MyDispatcher', 'Routing');
$Dispatcher = new MyDispatcher();
4) Update the $helper parameter in your controllers to use MyCache instead of Cache.
That's it. A little bit complicated, but it works as expected! Now you can adjust the $prefix to whatever you need and create unique cache files for e.g. different domains.

Better solution: CakePHP 2.3 now supports a cache prefix in core config:
Configure::write('Cache.viewPrefix', 'YOURPREFIX');
This prefix may be adapted to match the theme name or some other parameters that are different in those requests.

Related

Codeigniter HMVC asset managment

I am trying to give a shot to HMVC in Codeigniter. Here is my folder structure.
-ROOT
--APPLICATION
---MODULES
----Module_Email
-----Controllers
-----Models
-----Views
-----Assets
------JS
------CSS
------IMG
To render the Module i have to use
Module::run('Module_Email');
This method will output the rendered output, an example is given below
<script type="text/javascript" src="PATH/TO/EMAIL_MODULE/JS/JS_FILE.JS"></script>
<div data-module-name="Module_Email" class="Email_wrapper">
//RENDERED HTML CONTENT
</div>
Now here my problem start. Normally i would like to put all my resources to header. So when i call any module, its dependence need to be added in header instead of from where its get called.
I searched a lot but i couldn't find any good methods.
Please help.
Update
Currently i have a function on my header called get_assets() which will output predefined resources to header. But i cant say which modules is going to use in pages, so the system need to check which modules are used in this page, and if its used then its dependencies need to be added on header.
Seems like your main problem then is trying to figure out what modules were used.
Unfortunately as far as I can tell with the default Wiredesignz modular extension there is no way to access the module name unless you write some sort of hack to get at that data. The module being used is stored in the protected variable $module in the MX_Router class, however, there is no public method to allow you to get access to it. So your only choice would be to extend the class and create a public function.
Alternatively you could use a forked version of Wiredesignz implementation which I did which provides numerous other features including a public function to get at the $module variable. Using the forked version I wrote you could then use code such as this:
<?php $module_name = $this->router->fetch_module(); ?>
However, that will only record the last module you loaded, so you would still need to do work to store all the modules, and then have your function use this information to determine what assets to load. If I were doing something like you I would probably fork my version and then create an additional data structure to store every module that was loaded that you could then later get access to.
I don't think this is exactly what you were hoping for, but might be something to get you on the right track to finding a solution.
I added an array to the Module class to store the assets and two functions to store/retrieve the items. Here is the source (updated Modules.php)
# Register your assets
public static function register_asset( $asset )
{
if( in_array($asset,self::$assets) === FALSE )
{
self::$assets[] = $asset;
}
}
public static function assets()
{
return self::$assets;
}
and now you can register your assets like this inside your module
Modules::register_asset('myslider.js');
You can retrieve all your assets using
Modules:assets();
Which will return an array of assets that can be processed depending up on the situation.

Changing the behaviour of view in Codeigniter

I am using codeigniter for a project that is used by a variety of companies.
The default version of our software is up and running and works fine - however some of our customers want slightly different view files for their instance of the system.
Ideally what I would like to do is set a variable (for example VIEW_SUFFIX) and whenever a view file is loaded it would first check if there was a suffix version available if there was use that instead.
For example if the system had a standard view file called 'my_view.php' but one client had a VIEW_SUFFIX of 'client_1' - whenever I called $this->load->view('my_view') if the VIEW_SUFFIX was set it would first check if my_view_client_1 existed (and if it did use that) or if not use the default my_view.php.
I hope that my question is clear enough... If anyone has done this before or can think of a way to do it I would really appreciate it.
EDIT:
Ideally I would like a solution that works without me changing every place that I am calling the view files. Firstly because there are a few files that may want different client versions and also because the view files are called from a lot of controllers
I had a similar requirement for which I created a helper function. Among other things, this function can check for a suffix before loading the specified view file. This function can check for the suffix and check if the file exists before loading it.
Unfortunately, the file checking logic would be a bit brittle. As an alternative, you can implement a MY_Loader class that will override the basic CI_Loader class.
Something like this in your application/core/MY_Loader.php:
class MY_Loader extends CI_Loader {
protected function _ci_load($_ci_data)
{
// Copy paste code from CI with your modifications to check prefix.
}
}
Could you not do this
// some method of creating $client
// probably created at login
$_SESSION['client'] = 'client_1';
$client = (isset($_SESSION['client'])) ? $_SESSION['client'] : '';
$this->load->view("your_view{$client}", $data);

Get all "pages" in YII?

I'm trying to create my own xml sitemap. Everything is done except for the part that I thought was going to be the easiest. How do you get a list of all the pages on the site? I have a bunch of views in a /site folder and a few others. Is there a way to explicitly request their URLs or perhaps via the controllers?
I do not want to make use of an extension
You can use reflection to iterate through all methods of all your controllers:
Yii::import('application.controllers.*');
$urls = array();
$directory = Yii::getPathOfAlias('application.controllers');
$iterator = new DirectoryIterator($directory);
foreach ($iterator as $fileinfo)
{
if ($fileinfo->isFile() and $fileinfo->getExtension() == 'php')
{
$className = substr($fileinfo->getFilename(), 0, -4); //strip extension
$class = new ReflectionClass($className);
foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
{
$methodName = $method->getName();
//only take methods that begin with 'action', but skip actions() method
if (strpos($methodName, 'action') === 0 and $methodName != 'actions')
{
$controller = lcfirst(substr($className, 0, strrpos($className, 'Controller')));
$action = lcfirst(substr($methodName, 6));
$urls[] = Yii::app()->createAbsoluteUrl("$controller/$action");
}
}
}
}
You need to know what content you want to include in your sitemap.xml, I don't really think you want to include ALL pages in your sitemap.xml, or do you really want to include something like site.com/article/edit/1 ?
That said, you may only want the result from the view action in your controllers. truth is, you need to know what you want to indexed.
Do not think in terms of controllers/actions/views, but rather think of the resources in your system that you want indexed, be them articles, or pages, they are all in your database or stored somehow, so you can list them, and they have a URI that identifies them, getting the URI is a matter of invoking a couple functions.
There are two possiblities -
Case 1:
You are running a static website then you can find all your HTML inside 1 folder - protected/views/site/pages
http://www.yiiframework.com/wiki/22/how-to-display-static-pages-in-yii/
Case 2:
Website is dynamic. Tasks such as generating and regenerating Sitemaps can be classified into background tasks.
Running background taks can be achieved by emulating the browser which is possible in linux using - WGET, GET or lynx commands
Or, You can create a CronController as a CConsoleCommand. How to use Commands in YII is shown in link below -
http://tariffstreet.com/yii/2012/04/implementing-cron-jobs-with-yii-and-cconsolecommand/
Sitemap is an XML which lists your site's URL. But it does more than that.
It helps you visualize the structure of a website , you may have
category
subcategories.
While making a useful extension, above points can be kept into consideration before design.
Frameworks like Wordpress provide way to generate categorical sitemap.
So the metadata for each page is stored from before and using that metadata it discovers and group pages.
Solution by Reflection suggested by #Pavle is good and should be the way to go.
Consider there may be partial views and you may or may not want to list them as separate links.
So how much effort you want to put into creating the extension is subject to some of these as well.
You may either ask user to list down all variables in config fie and go from there which is not bad or you have to group pages and list using some techniques like reflection and parsing pages and looking for regex.
For ex - Based on module names you can group them first and controllers inside a module can form sub-group.
One first approach could be to iterate over the view files, but then you have to take into account that in some cases, views are not page destinations, but page sections included in another pages by using CController::renderPartial() method. By exploring CController's Class Reference I came upon the CController::actions() method.
So, I have not found any Yii way to iterate over all the actions of a CController, but I used php to iterate over all the methods of a SiteController in one of my projects and filter them to these with the prefix 'action', which is my action prefix, here's the sample
class SiteController extends Controller{
public function actionTest(){
echo '<h1>Test Page!</h1></p>';
$methods = get_class_methods(get_class($this));
// The action prefix is strlen('action') = 6
$actionPrefix = 'action';
$reversedActionPrefix = strrev($actionPrefix);
$actionPrefixLength = strlen($actionPrefix);
foreach ($methods as $index=>$methodName){
//Always unset actions(), since it is not a controller action itself and it has the prefix 'action'
if ($methodName==='actions') {
unset($methods[$index]);
continue;
}
$reversedMethod = strrev($methodName);
/* if the last 6 characters of the reversed substring === 'noitca',
* it means that $method Name corresponds to a Controller Action,
* otherwise it is an inherited method and must be unset.
*/
if (substr($reversedMethod, -$actionPrefixLength)!==$reversedActionPrefix){
unset($methods[$index]);
} else $methods[$index] = strrev(str_replace($reversedActionPrefix, '', $reversedMethod,$replace=1));
}
echo 'Actions '.CHtml::listBox('methods', NULL, $methods);
}
...
}
And the output I got was..
I'm sure it can be furtherly refined, but this method should work for any of the controllers you have...
So what you have to do is:
For each Controller: Filter out all the not-action methods of the class, using the above method. You can build an associative array like
array(
'controllerName1'=>array(
'action1_1',
'action1_2'),
'controllerName2'=>array(
'action2_1',
'action2_2'),
);
I would add a static method getAllActions() in my SiteController for this.
get_class_methods, get_class, strrev and strlen are all PHP functions.
Based on your question:
1. How do you get a list of all the pages on the site?
Based on Yii's way of module/controller/action/action_params and your need to construct a sitemap for SEO.
It will be difficult to parse automatically to get all the urls as your action params varies indefinitely. Though you could simply get controller/action easily as constructed by
Pavle Predic. The complexity comes along when you have customized (SERF) URL rules meant for SEO.
The next best solution is to have a database of contents and you know how to get each content via url rules, then a cron console job to create all the urls to be saved as sitemap.xml.
Hope this helps!

Advanced Theme support

I have to implement an ability of using themes. I tried https://github.com/liip/LiipThemeBundle and it works.
But because i'm using multisite feature (i.e. site_id is being defined depending on domain name) the paths should be like these:
app/sites/{site_id}/Resources/themes/phone/BundleName/template.html.twig
app/sites/{site_id}/Resources/BundleName/views/template.html.twig
src/BundleName/Resources/themes/phone/template.html.twig
src/BundleName/Resources/views/template.html.twig
And if i request ::template.html.twig paths would be:
app/sites/{site_id}/Resources/themes/phone/template.html.twig
app/sites/{site_id}/Resources/views/template.html.twig
So the questions are:
The site_id only can be fetched when runtime $container->get('engine.site')->getId(). I tried to get inside the Liip\ThemeBundle\Locator\FileLocator's constructor and tweak $this->path there to be able to get a path for different sites of a view app/Resources/sites/{site_id}/ (but would be super to get app/sites/{site_id}/Resources/). But this way we of course will get an error when first request and the cache is not formed yet («Whoops, looks like something went wrong. InactiveScopeException: You cannot create a service ("request") of an inactive scope ("request").»). And every other request will work out normally. But this is really not a solution. From this the question is where can i dynamically build paths for templates?
all the themes in LiipThemeBundle can be set up in config. But how do i make this list dynamically (all themes we can fetch with $container->get('engine.site')->getThemes())?
LiipThemeBundle creates parameters liip_theme.file_locator.class which holds the name of the class (with locator).
So we have to change this parameter in our bandle's DI to the name of our class. And its description is quite simple:
<?php
namespace Acme\Bundle\EngineBundle\Locator;
use Liip\ThemeBundle\Locator\FileLocator as BaseFileLocator;
class MultisitesFileLocator extends BaseFileLocator
{
public function locateAppResource($name, $dir = null, $first = true)
{
$container = $this->kernel->getContainer();
if ($container->getParameter('engine.dir_sites') !== '') {
$dir = $container->getParameter('kernel.root_dir') . '/' .
$container->getParameter('engine.dir_sites') .
$container->get('engine.site')->getId() . '/Resources';
}
return parent::locateAppResource($name, $dir, $first);
}
}
However multisite feature doesn't work this way...
But it's not a big deal.

CakePHP Cache::write() Can keys be grouped by model?

Consider the following;
Cache::write('Model.key1' , 'stuff');
Cache::write('AnotherModel.key1' , 'stuff');
Cache::write('Model.key2' , 'stuff');
Can I delete a group of keys from the Cache?
For instance, if I wanted to clear all cached data for "Model" but leave "AnotherModel" in the cache, I would like to use the following;
Cache::delete('Model.*');
Can this kind of thing be achieved in CakePHP 1.3.x?
Thanks!
For those just Googling this issue (as I was), Cake 2.2 now supports this kind of functionality (without having to create separate cache configs for each 'group').
There is a little explanation here, although it lacks some details:
http://book.cakephp.org/2.0/en/core-libraries/caching.html#using-groups
But this is what I did in my app and it appears to work well. ;-)
In /app/Config/core.php
Cache::config('default', array(
'engine' => $engine,
...
'groups' => ['navigation'],
));
Model afterSave hook:
function afterSave($created) {
// This deletes all keys beginning with 'navigation'
Cache::clearGroup('navigation');
parent::afterSave($created);
}
Then in my controller/model that requires an expensive query...
// We create a unique key based on parameters passed in
$cacheKey = "navigation.$sitemapId.$levelsDeep.$rootPageId";
$nav = Cache::read($cacheKey);
if (!$nav) {
$nav = $this->recursiveFind(
'ChildPage',
['page_id' => $rootPageId],
$levelsDeep
);
Cache::write($cacheKey, $nav);
}
You can set up a different cache config for each model such that each config has a different path.
http://book.cakephp.org/view/1515/Cache-config
I don't see a way to completely delete all data using the cache class but you could write your own function to delete all of the data (cached files) out of the specific cache directory for the specific model. By using different config files for different models, you can isolate the model's cached data into a unique directory for each model and then manually delete the contents when you want to flush the data for that model.

Categories