Views in CakePHP can be arranged in themes. We use this to define our design-specific templates file in directory app/View/Themed/Bootstrap. Each controller action has a corresponding view template, e.g. the file app/View/Projects/index.ctp. CakePHP uses plain PHP files as templates. Here is some example code from index.ctp:
<?php foreach ($columns as $column): ?>
<div class="span<?php echo $width; ?>">
<?php foreach ($column as $project): ?>
<div class="project">
<h4>
<?php echo $this->Html->link($project['Project']['title'], array('action' => 'view', $project['Project']['id'])); ?>
</h4>
<?php echo $this->Html->link(
$this->Thumbnail->render('project/' . $project['Project']['id'] . '.jpg', array(
'width' => 200, 'height' => 200, 'resizeOption' => 'auto',
)), array('action' => 'view', $project['Project']['id']), array('escape' => FALSE, 'class' => 'thumbnail')); ?>
<p class="about"><?php echo $this->Text->truncate($project['Project']['about'], /*length*/100 + rand(1, 100), array('exact' => FALSE)); ?></p>
</div>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
<?php echo $this->element('paginate'); ?>
$columns is the paginated list of projects, arranged into columns and passed from the controller. $this->Html is a view helper that we use here to generate links. $this->Thumbnail is a custom helper. This is defined in app/View/Helper/ThumbnailHelper.php and declared in app/Controller/AppController.php as
<?php
class ProjectsController extends AppController {
public $helpers = array('Thumbnail', 'Markdown.Markdown');
// …
}
Markdown formatting for project detail pages is provided by the Markdown plugin. The helpers Html and Text are always available and don’t have to be declared this way.
To provide a common design, the output of templates is generally embedded into layout files. The basic structure of our layout file app/View/Themed/Bootstrap/Layouts/default.ctp looks like this:
<!DOCTYPE html>
<html>
<head>
<?php echo $this->Html->charset(); ?>
echo $this->Html->css('/bootstrap/css/bootstrap.min');
echo $this->Html->css('styles');
?>
</head>
<body class="<?php echo strtolower($this->name . '-' . $this->action); ?>">
<div class="container-narrow">
<!-- … more markup -->
<?php echo $this->Session->flash(); ?>
<?php echo $this->fetch('content'); ?>
</div><!-- .container-narrow -->
</body>
</html>
The content of the view template is inserted by the call to
<?php
$this->fetch('content');
The Session helper outputs to feedback to the user like e.g. a success message when saving a project.
In Django, templates can be application specific or global. Here is an excerpt from our file templates/index.html:
{% extends 'layout.html' %}
{% load markup %}
{% block content %}
<div class="row-fluid marketing projects">
{% for column in columns %}
<div class="span{{ width }}">
{% for project in column %}
<div class="project">
<h4>
<a href="{% url 'project' project.id %}">
{{ project.title }}
</a>
</h4>
<a href="{% url 'project' project.id %}">
{% if project.photo %}
<img class="project-detail" src="{{ project.photo_medium.url }}">
{% else %}
<img class="project-detail" src="/static/img/ni.png">
{% endif %}
</a>
<p>{{ project.about|truncatewords:"40" }}</p>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% include '_paginate.html' with projects=projects %}
{% endblock %}
Django has its own template language. In this example, the columns variable is provided by the view. Function calls to url use named routes with parameters to generate proper URIs. Scaled photos are declared in the model and can be accessed as ordinary model fields, e.g. project.photo_medium.url. The code for a paginator at the bottom of the page is included from another template file.
Most of the view code in our Drupal implementation is generated by the system. To make the design match our other implementations, we have created our own theme in sites/themes/fuxcon2013 and have provided some template files and preprocessor functions there. This is the contents of our theme directory:
File fuxcon2013.info is the only file required for a theme. It contains basic declaration and add our own style sheets and javascript to Drupal’s output:
name = FUxCon2013
description = A theme based on Twitter Bootstrap for FUxCon2013.
package = FUxCon2013
core = 7.x
stylesheets[all][] = bootstrap/css/bootstrap.min.css
stylesheets[all][] = styles.css
scripts[] = bootstrap/js/bootstrap.min.js
The file template.php contains so called preprocessor functions. These manipulate variables that Drupal provides to the HTML it generates. Here is an example:
<?php
function fuxcon2013_process_page(&$vars) {
$vars['title'] = NULL;
$vars['page']['header'][] = array('#markup' => theme('header', $vars));
}
This removes the standard title from all pages and adds a custom header from file header.tpl.php. Drupal templates are standard PHP that gets variables passed by Drupal. For example, this is file views-view-unformatted–projects.tpl.php which arranges projects into columns:
<?php
// krumo(get_defined_vars());
$columns = array();
foreach ($rows as $i => $row) {
$col = $i % FUXCON_NO_COLS;
$columns[$col][] = $row;
}
?>
<?php foreach ($columns as $column): ?>
<div class="span<?php echo FUXCON_COL_WIDTH; ?>">
<?php foreach ($column as $project): ?>
<?php echo $project; ?>
<?php endforeach; ?>
</div>
<?php endforeach; ?>
The most difficult part in theme development for Drupal is to know which files to overwrite and what variables get passed into these. In this example, we overwrite a template for the Views module. There also is a handy debug function that allows us to see all variables passed into the template:
<?php
krumo(get_defined_vars());
Here is a screenshot from the admin interface of the Views module that shows which templates get loaded:
Symfony provides its own template language called Twig which very closely resembles the Django template language. Here is an excerpt from the project list in src/FUxCon2013/ProjectsBundle/Resources/views/Projects/index.html.twig:
{% extends 'FUxCon2013ProjectsBundle::layout.html.twig' %}
{% block content %}
<div class="row-fluid marketing projects">
{% for column in columns %}
<div class="span{{ width }}">
{% for project in column %}
<div class="project">
<h4>
<a href="{{ path('_project', { 'id': project.id } ) }}">{{ project.title }}</a>
</h4>
<a href="{{ path('_project', { 'id': project.id } ) }}">
<img src="{{ thumbnail([ '/images/projects/', project.id, '.png' ] | join, '200x200') }}">
</a>
<p>{{ project.about | truncate }}</p>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% include 'FUxCon2013ProjectsBundle::_paginate.html.twig' with { 'default_page': '_projects' } %}
{% endblock %}
Symfony uses named routes to generate URIs through the path() function. Loop and conditional constructs look like their Python cousins.
This template uses extension to embed its markup into a common layout. The general structure of our layout file src/FUxCon2013/ProjectsBundle/Resources/views/layout.html.twig looks like this:
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>{% block title %}Projects{% endblock %}</title>
<link href="{{ asset('bundles/fuxcon2013/bootstrap/css/bootstrap.min.css') }}" rel="stylesheet">
<link href="{{ asset('bundles/fuxcon2013/css/sites.css') }}" rel="stylesheet">
</head>
<body>
<div class="container-narrow">
{% for flashMessage in app.session.flashbag.get('notice') %}
<div class="flash-message">{{ flashMessage }}</div>
{% endfor %}
{% block content %}
{% endblock %}
</div> <!-- /.container-narrow -->
</body>
</html>
The template views/Projects/index.html.twig provides overwrites for the blocks defined in this layout file.
Twig can be extended with custom tags and functions. We provide a new function thumbnail() and a filter truncate through such an extension. The extension lives in file src/FUxCon2013/ProjectsBundle/Twig/Extension/FUxCon2013Extension.php:
<?php
namespace FUxCon2013\ProjectsBundle\Twig\Extension;
use Symfony\Bundle\TwigBundle\Extension\AssetsExtension;
use Symfony\Component\DependencyInjection\ContainerInterface;
class FUxCon2013Extension extends \Twig_Extension
{
private $container;
public function __construct(ContainerInterface $container)
{ $this->container = $container; }
public function getFunctions()
{
return array(new \Twig_SimpleFunction('thumbnail', array($this, 'thumbnail')));
}
public function getFilters()
{
return array(new \Twig_SimpleFilter('truncate', array($this, 'truncate')));
}
/**
* Provide a new template function that generates a cacheable, derived image
*/
public function thumbnail($path, $size = null)
{
// some hairy code to generate the derived image
}
/**
* Provide a filter that truncates $text at word borders
*/
public function truncate($text, $length = 150)
{
if (strlen($text) < $length) {
return $text;
}
$text = substr($text, 0, $length);
$blank = strrpos($text, ' ');
if (FALSE === $blank) {
$text = '';
}
else {
$text = substr($text, 0, $blank);
}
return $text . ' ...';
}
public function getName()
{ return 'fuxcon2013_extension'; }
}
To be recognized, it needs to be registered with the system in app/config/config.yml like so:
services:
fuxcon2013.twig.fuxcon2013_extension:
class: FUxCon2013\ProjectsBundle\Twig\Extension\FUxCon2013Extension
tags:
- { name: twig.extension }
arguments: [ @service_container ]
Now that we have covered the basic building blocks of a model-view-controller architecture, we go on to describe the features specific to our requested features, namely user accounts, tagging, picture uploading and scaling, creating and updating projects, Markdown formatting, and finally the generation of test data.