CakePHP uses entries in the file app/Config/routes.php to map request URIs to Controller methods.
Here is the route declared for our projects in app/Config/routes.php:
<?php
Router::connect('/', array('controller' => 'projects', 'action' => 'index'));
This declares the homepage to be processed by controller method ProjectsController.index(). We rely on the default format for routes, e.g. /projects/view/32 for the other routes in our implementation.
Controllers are classes located in a file in app/Controller. Here is part of our ProjectsController.php:
<?php
class ProjectsController extends AppController {
const NO_COL = 3;
const PAGE_SIZE = 5;
public $paginate = array(
'Project' => array(
'limit' => self::PAGE_SIZE,
)
);
public $helpers = array('Thumbnail', 'Markdown.Markdown');
public function index() {
$this->Project->recursive = 0;
$this->set('title_for_layout', 'Projects');
$projects = $this->paginate('Project');
$columns = array();
foreach ($projects as $i => $project) {
$col = $i % self::NO_COL;
$columns[$col][] = $project;
}
$this->set(array(
'columns' => $columns,
'width' => 12 / self::NO_COL,
));
}
// … more actions
}
To make our example a bit more interesting, this code gets a paginated list of projects from the database and arranges this into columns. The controller uses $this->set() to pass variables to its associated view (see below) which then renders it to the client browser. Pagination is a built-in feature that gets configured by an instance variable $paginate and uses the method paginate() to get a page worth of project objects.
Controllers declare certain aspects of their operation as instance variables. Configuration shared with other controllers can be inherited from a common base class AppController. For example, our ProjectsController inherits the design appearance, and components needed for authentication from this base class:
<?php
class AppController extends Controller {
public $theme = 'Bootstrap';
public $components = array(
'DebugKit.Toolbar',
'Session',
'Auth' => array(
'loginRedirect' => array('controller' => 'projects', 'action' => 'index'),
'logoutRedirect' => array('controller' => 'projects', 'action' => 'index'),
'authorize' => array('Controller'),
'element' => 'message-auth',
)
);
public function beforeFilter() {
$this->Auth->allow('index', 'view');
}
// … more common methods
}
Django configures URLs for an app in its file urls.py. Here is an excerpt from our projects/urls.py:
urlpatterns = patterns('projects.views',
url(r'^$', 'index', name='project_list'),
)
urlpatterns += patterns('',
url(r'^project/(?P<pk>\d+)/$', ProjectDetailView.as_view(), name='project'),
url(r'^project/add/$', ProjectAddView.as_view(), name='add_project'),
url(r'^project/edit/(?P<pk>\d+)/$', ProjectEditView.as_view(), name='edit_project'),
url(r'^user/', include(django.contrib.auth.urls)),
)
URLs are named to be able to generated named URLs in views. Views can be methods in our own implementation (e.g. the function projects.views.index() ) or can be classes from other apps. It is also possible to mount while ranges of URLs under a common root as is done in our example for the user handling.
Here is the view function for listing projects in file projects/views.py:
GRID_COL = 12
NO_COL = 3
PER_PAGE = 5
def index(request):
project_list = Project.objects.all()
paginator = Paginator(project_list, PER_PAGE)
page = request.GET.get('page')
try:
projects = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
projects = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
projects = paginator.page(paginator.num_pages)
columns = [[] for x in range(0, NO_COL)]
for i, project in enumerate(projects):
col = i % NO_COL;
columns[col].append(project);
tags = Project.tags.most_common()
return render(request, 'projects/index.html', {
'body_class': 'projects-index',
'columns': columns,
'projects': projects,
'tags': tags,
'width': GRID_COL / NO_COL,
})
Pagination uses the class Paginator from the system component django.core.paginator.
Our implementation does not use hand-written code for the business logic at all but relies on Drupal core or extensions like the Views module to provide it.
Here is a screen shot from the view definition for the project list in Drupal’s admin inteface:
The Views module needs to be installed as an extension. This can conveniently be done using the Drush shell:
mkdir sites/all/modules/{custom,contrib}
drush download views
drush enable views
This downloads Views into the directory sites/all/modules/contrib/views and enables it.
As with content types, views can be exported as code to more easily manage them. However, we don’t do this in our implementation.
Controllers in Symfony are classes in the bundle of your application. In our example, this is the file src/FUxCon2013/ProjectsBundle/Controller/ProjectsController.php. Here is an excerpt:
<?php
class ProjectsController extends Controller
{
const NO_COL = 3;
const PAGE_SIZE = 5;
/**
* @Route("/", defaults={"offset" = 1})
* @Route("/page:{offset}", name="_projects")
* @Template()
*/
function indexAction(Request $request, $offset = 1)
{
$repo = $this
->getDoctrine()
->getManager()
->getRepository('FUxCon2013ProjectsBundle:Project');
$limit = 10;
$from = (($offset * $limit) - $limit);
$totalCount = $repo->count();
$totalPages = ceil($totalCount / $limit);
$projects = $repo->findPaginated($from, $limit);
$columns = array();
foreach ($projects as $i => $project) {
$col = $i % self::NO_COL;
$columns[$col][] = $project;
}
$vars = array(
'columns' => $columns,
'width' => 12 / self::NO_COL,
'page' => $offset,
'totalPages' => $totalPages,
'body_class' => 'projects-index',
);
return $vars;
}
// … more action methods
}
We use the option to declare routes as annotations in comments. In this example, the method ProjectsController.indexAction() is accessed through the routes
<?php
/**
* @Route("/", defaults={"offset" = 1, "tag" = null})
* @Route("/page:{offset}", name="_projects")
*/
Symfony does not have pagination built in so we use our custom methods from the repository to get at the count and the paginated list of projects.
The annotation @Template() signals to Symfony that we want the variables returned from the controller to be rendered by a template with a standard name, in our case the file src/FUxCon2013/ProjectBundle/Resources/views/Projects/index.html.twig. We show this file below when explaining the views.
A powerful concept related to controllers are parameter converters. Initially, parameters passed to actions are strings derived from an associated route. In many cases however, they denote domain objects. The process of finding a domain object from an URL parameter can be delegated out if the controller by using a dedicated parameter converter. We make use of this mechanism in our controller. Here is the declaration of our ProjectController.showAction() to display a single project:
<?php
/**
* Finds and displays a Project entity.
*
* @Route("/project/{id}", name="project_show")
* @Method("GET")
* @Template()
*
* Uses type hint "Project $project" to implicitely invoke ParamConverter
*/
public function showAction(Project $project)
{}
Symfony is even smart enough to derive the need for a parameter converter from the type hint given to the parameter. Even more so, there is a built-in doctrine.orm converter that fetches an object from the database based on its primary key. So, in our simple example, Symfony can get our project completely on its own. If a matching project is not found, a 404 response is generated.