Migrating from Gryphon to CEO Gryphon Compatibility Layer
Gryphon Compatibility Layer seeks to provide a fully Gryphon compatible environment, but some changes are necessary to support updates the the underlying Twig templating system.
Luckily, there's a command to find all the issues listed below. Simply run the following and CEO will report any issues with your templates:
php run twig:lint path/to/templates
Things you must do
All of the following will cause errors in your template and should be fixed right away. Fortunately, these are all backward compatible so making a these changes before moving to CEO will not cause errors in the Gryphon site.
Macro inheritance changes in Twig 2.x
Macro inheritance was, apparently, a bug in Twig 1.x that has been "fixed" in Twig 2.x. That means you can no longer have a macro import in your base template and expect it to be available in the child templates. The macro must be imported into each scope that requires it.
BAD
base.tpl:
{% import "macros/variables.tpl" as variables %}
{% block title %}{% endblock %}
{% block content %}{% endblock %}
view.tpl:
{% extends "base.tpl" %}
{% block title %}
{{ variables.getTitle() }}
{% endblock %}
{% block content %}
{{ variables.getDescription() }}
{% endblock %}
GOOD
base.tpl:
{% block title %}{% endblock %}
view.tpl:
{% extends "base.tpl" %}
{% block title %}
{% import "macros/variables.tpl" as variables %}
{{ variables.getTitle() }}
{% endblock %}
{% block content %}
{% import "macros/variables.tpl" as variables %}
{{ variables.getDescription() }}
{% endblock %}
Escaping output
Twig 2.x no longer automatically escapes HTML for you, that means any place in your template that outputs raw HTML must be escaped with the raw
filter.
BAD
{{ article.content_formatted }}
GOOD
{{ article.content_formatted|raw }}
Fetching with uid
When fetching with uid
, be sure to namespace it with self:
to avoid confusion. The primary key field is ambiguous and can cause errors when selecting content.
BAD
{% fetch articles from article with {
'where': 'uid = 1',
'limit': 1
} %}
GOOD
{% fetch articles from article with {
'where': 'self:uid = 1',
'limit': 1
} %}
uid
should always be lowercase
CEO is case-sensitive, and calls to uid
should always be lowercase as internally, the Gryphon Compatibility Layer converts uid
to the CEO model's internal primary key, which may be different.
BAD
{% fetch articles from article with {
'where': 'UID = 1',
'limit': 1
} %}
GOOD
{% fetch articles from article with {
'where': 'self:uid = 1',
'limit': 1
} %}
Some filters have been removed
random
You need to use therandom
function instead. For example:
{{ '0'|random }}
becomes{{ random() }}
toTime
Use thedate
anddate_modify
filters instead. For example:
{{ "-1 day"|toTime }}
becomes{{ 'now'|date_modify('-1 day')|date('u') }}
Server side browser sniffing is gone
Look... it was bad, it's not a good idea to use it, and it never really worked properly in the first place. Here are some possible work arounds:
Desktop and mobile layouts
Media queries, yo. Plus, Bootstrap, since v3, has had push/pull and column sizing. There's really no reason not to.
Display different content based on device
See above.
Display same ad code in different places
You can actually do this, you just have to plan ahead. Let's say, you have the following ad tag in your header or GTM block:
googletag.defineSlot('/1234/XXX_AdhesionsBanner_Mobile_320x50', [320, 50], 'div-gpt-ad-1234567890-0').addService(googletag.pubads());
And the ad code looks like this:
<div id='div-gpt-ad-1234567890-0' style='width:320px; height:50px; margin: 0 auto;'>
<script type='text/javascript'>
googletag.cmd.push(function()
{ googletag.display('div-gpt-ad-1234567890-0'); }
);
</script>
</div>
All you have to do is add another slot with a random number:
googletag.defineSlot('/1234/XXX_AdhesionsBanner_Mobile_320x50', [320, 50], 'div-gpt-ad-1234567890-0').addService(googletag.pubads());
googletag.defineSlot('/1234/XXX_AdhesionsBanner_Mobile_320x50', [320, 50], 'div-gpt-ad-1234567890-2').addService(googletag.pubads());
Then you'll be able to use another ad tag with that id:
<div id='div-gpt-ad-1234567890-2' style='width:320px; height:50px; margin: 0 auto;'>
<script type='text/javascript'>
googletag.cmd.push(function()
{ googletag.display('div-gpt-ad-1234567890-2'); }
);
</script>
</div>
Things you should do
The following items should be done to make the transition away from the Compatibility Layer easier in the future.
Macro scope
Like inserting the macro imports into each scope they're required, it's also a good idea to remove any imports that aren't being called anywhere. The lint command will warn you of any unused imports.
Base meta
Sites should switch to the bundled CEO meta template, instead of using their own. This will eensure maximum compatibility with social media services. It is usually as simple as substituting the site's meta properties for the following:
{% include 'helpers/meta.twig' %}
Basic variables
If you do use the bundled meta template, you need to ensure the variables macro has the correct macros defined. The lint command will warn you if any are missing.
Search Results
Sites should implement the bundled search results template, unless there is a good reason not to. This is as simple as adding the following to the search/advanced.tpl
, in place of the current result list:
{% include helpers/search-results.twig %}
Spaces not tabs
Srsly. Don't use tabs. Luckily for you, we have a command to convert your files:
php run twig:detab --tab-stop=4 path/to/templates
Migrating view callbacks
The one item the Gryphon Compatibility Layer doesn't provide a direct upgrade path for is custom view callbacks. These customizations, which live in the site's template/gryphon/view
folder will need to be converted to CEO Interceptors.
Luckily 99% of sites do not use any custom view callbacks, and those that do use a standard set. Below are a number of common customizations found in Gryphon view callbacks and how to translate them to Ceo Interceptors.
Breaking news
Note: this has been done in the default IndexInterceptor
, but if you need it for other pages, here is how to handle breaking news.
To handle loading breaking news, you'll likely see something like this in the view/main.view.php
callback:
function main($container, $payload, $kwargs=array()) {
...
// check for breaking news
$payload['breaking'] = false;
$breaking = M::init('article')
->cache(false)
->where('self:status = 1')
->order('self:created desc')
->limit(1)
->findByTags(M::init('tag')->findByName('breaking'))
->pop();
if( $breaking && $breaking->uid ) {
$payload['breaking'] = $breaking;
}
...
}
This can be replicated in an interceptor using the beforeRender
method:
public function beforeRender($params = [])
{
if (!isset($params['breaking'])) {
$breaking = false;
$tag = $this->getDI()->getTagManager()
->find(['name = "breaking"']);
$breaking = $this->getDI()->getArticleManager()
->getBuilder()
->wherePublished()
->orderBy('published_at desc')
->limit(1)
->withTags($tag)
->find();
if ($breaking) {
// make sure to wrap the returned object in a compatibility model
$params['breaking'] = new \Ceo\Compat\Model\Article($breaking[0]);
}
}
return $params;
}
Multimedia selection and pagination
Gryphon handled YouTube, Vimeo and plain video differently, CEO does not. But, there may be cases where you need to override what is returned from the media controller and provide pagination back to the view.
A common example from Gryphon looks like this:
function main($container, $payload, $kwargs=array()) {
$slug = $payload['slug'];
if( $slug == 'video' ) {
$id = false;
$slug = false;
$topMedia = false;
$limit = 20;
$start = 0;
$page = $container["Request"]->get('page', 'num');
if( $page && $page >= 0 ) {
$start = $page * $limit;
}
if( !($id = $container["Request"]->get(':id', 'num')) ) {
$id = $container["Request"]->get(':slug', 'num');
}
$slug = $container["Request"]->get(':slug', 'specialChars');
$media = M::init('gryphon:media');
if( !\admin\lib\auth::hasSession() ) {
$media->where('self:status = 1');
}
$media = $media
->order('self:created desc')
->limit($start.', '.$limit);
$tags = M::init('gryphon:tag')->findByName('multimedia');
$media = $media
->where('self:type = "youtube" or self:type = "vimeo"')
->find();
$url = 'gryphon:multimedia';
if( $slug ) {
$url .= '/'.$slug;
}
if( $id ) {
$url .= '/'.$id;
}
$pag = new \foundry\model\paginator($media, $page, $limit, 5);
$pag->setURL($url, array(
'page' => '%PAGE%'
));
if( $topMedia ) {
$media->pop();
$media->unshift($topMedia);
}
$payload['media'] = $media;
$payload['pagination'] = $pag;
}
...
}
In CEO, you could do the following:
public function beforeRender($params = [])
{
if ($params['slug'] == 'video') {
$page = $this->request->getQuery('page', 'int', 0);
$perPage = $this->request->getQuery('per_page', 'int', 20);
$tag = $this->getDI()->getTagManager()->findFirst([
'name = "multimedia"'
]);
$builder = $this->getDI()->getMediaManager()->getBuilder()
->where(["type in ('video', 'youtube', 'vimeo')"])
->setPage($page)
->setLimit($perPage)
->byTags([$tag]);
$builder = $builder->paginate();
$params['media'] = $builder->getItems();
$params['pagination'] = $builder->getPagination();
}
}
Dynamic loading
Many times the view callback is used to load a different template to handle infinite loading for a section. While in CEO you could just add another route and endpoint, you can still do this the old school way.
In Gryphon, in the site's section template, you might have a call like this:
// load section
$.get('/section/sports.html?ajax=1', ...)
In the view callback, you had something like:
function main($container, $payload, $kwargs=array()) {
...
$post = $container["Request"]->any('post', 'num');
$template = false;
if ($post) {
$template = 'section/_dynamic_load.tpl';
}
...
try {
$tpl = new Template($template);
$res = new Response;
$res->content = $tpl->render($payload);
} catch( \foundry\exception $e ) {
// couldn't locate template load the default
$tpl = new Template('section/main.tpl');
$res = new Response;
$res->content = $tpl->render($payload);
}
return $res;
}
In CEO, after making sure you've set up the client's Module.php
, override their section interceptor with a custom one, using their namespace (Abc
in this case):
'view' => [
...
'interceptors' => [
...
'/section/{slug}' => '\Abc\Interceptors\SectionInterceptor',
...
]
]
Then add your new interceptor, using the beforeRender
function:
templates/library/src/interceptors/SectionInterceptor.php:
<?php
namespace Abc\Interceptors;
use Ceo\Http\Response;
class SectionInterceptor extends \Ceo\Compat\Interceptors\SectionInterceptor
{
public function beforeRender($params = [])
{
$params = parent::beforeRender($params);
if ($this->request->getQuery('ajax')) {
$template = 'gryphon/section/_dynamic_load.tpl';
$partial = $this->getDI()->getTwigPartial();
$content = $partial->render($template, $params);
$resp = new Response;
$resp->setContent($content);
return $resp;
}
return $params;
}
}
JSON feeds
Another common use of view callbacks is to insert missing data into JSON feeds, like author or media data. While this is handled for you in CEO, you may need to alter the format.
A typical JSON callback in Gryphon may look like this:
function json($request, $payload, $kwargs=array()) {
for ($i=0; $i < $payload['articles']->length; $i++) {
$payload['articles'][$i]->media;
$payload['articles'][$i]->tags;
}
return \foundry\view\json($request, $payload, $kwargs);
}
In CEO, you could do the following:
public function beforeRenderJson($params = [])
{
$articles = $params['articles']->map(function($article) {
return $article->toArray([
'related' => [
'media',
'tags',
'authors'
]
]);
});
$response = new \Phalcon\Http\Response;
$response->setHeader('Content-Type', 'application/json');
$response->setHeader('E-Tag', md5($params['section']->modified_at));
$response->setContent(json_encode($articles, JSON_PRETTY_PRINT));
return $response;
}