Doctrine 1 Integration with Zend Framework
This project tries to offer a complete Integration of Doctrine 1 with Zend Framework. The following components belong to this Integration:
- Zend_Application Resource
- Zend Framework Modular Project Support
- Zend_Tool Provider for Doctrine Model Generation, Migrations and Fixtures
- Zend_Paginator Adapter for Doctrine Queries
- Dynamic Zend_Form generation from Doctrine Models
This integration requires the latest Doctrine version 1.2.2 to work completly
Installation
Get it!
SVN Export or Externals
Github offers SVN Read support for a while now, you can either use svn export or svn:externals to include ZFDoctrine into your project or into your PHP Include Path.
svn checkout http://svn.github.com/beberlei/zf-doctrine.git
Git Clone
git clone git://github.com/beberlei/zf-doctrine.git
Install ZF with Zend Tool
To get ZF with Zend-Tool support running you can execute the following (on Linux):
root@benny-pc:~# pear channel-discover pear.zfcampus.org
Adding Channel "pear.zfcampus.org" succeeded
Discovery of channel "pear.zfcampus.org" succeeded
root@benny-pc:~# pear install zfcampus/zf
downloading ZF-1.10.4.tgz ...
Starting to download ZF-1.10.4.tgz (3,528,732 bytes)
...................................done: 3,528,732 bytes
install ok: channel://pear.zfcampus.org/ZF-1.10.4
root@benny-pc:~# zf show version
Zend Framework Version: 1.10.4
Zend_Tool Configuration
To enable the ZFDoctrine Tool Providers you have to register them in your Zend Tool configuration. If you have ZFDoctrine in your include path this is as easy as calling:
benny@benny-pc:~$ zf enable config.provider ZFDoctrine_Tool_DoctrineProvider
Provider/Manifest 'ZFDoctrine_Tool_DoctrineProvider' was enabled for usage with Zend Tool.
If you don't have a .zf.ini
configuration file you can create one by calling:
benny@benny-pc:~$ zf create config
Successfully written Zend Tool config.
It is located at: /home/benny/.zf.ini
If you don't have ZFDoctrine in your include path, you need to configure Zend Tools
include path to do so. Go to your $HOME directory, open up .zf.ini
and add a line:
php.include_path = "include-path here"
Now check if the installation worked by calling:
zf ? doctrine
The result should be help information on all the available commands:
Zend Framework Command Line Console Tool v1.10.4
Actions supported by provider "Doctrine"
Doctrine
zf create-project doctrine dsn zend-project-style library-per-module single-library
zf build-project doctrine force load reload
zf create-database doctrine
zf drop-database doctrine force
zf create-tables doctrine
zf generate-sql doctrine
zf dql doctrine
zf load-data doctrine append
zf dump-data doctrine individual-files
zf generate-models-from-yaml doctrine
zf generate-yaml-from-models doctrine
zf generate-yaml-from-database doctrine
zf generate-migration doctrine class-name from-database from-models
zf excecute-migration doctrine to-version
zf show-migration doctrine
zf show doctrine
Some shells like Fish might require a:
zf '?' doctrine
in order to get that done.
Setting up a new Zend Framework Project
The Tool support for Doctrine 1 only works if your project contains a .zfproject.xml file that contains a "BootstrapFile" resource. For a new project you can easily achieve this by calling:
benny@benny-pc:/tmp$ zf create project my-project
Creating project at /tmp/my-project
Note: This command created a web project, for more information setting up your VHOST, please see docs/README
You should now import the ZFDoctrine and Doctrine 1.2 libraries into your
application-root/library folder to make them available in your projects include
path. Alternatively you can edit your application/configs/application.ini
and change the include path to search for both projects.
To convert an existing project to use ZFDoctrine is a bit more complicated and really not recommended. If the project does not have a .zfproject.xml you have to create one. If you follow the Zend Standards you can get away with the following file contents:
<?xml version="1.0"?>
<projectProfile type="default" version="1.10">
<projectDirectory>
<projectProfileFile filesystemName=".zfproject.xml"/>
<applicationDirectory classNamePrefix="Application_">
<configsDirectory>
<applicationConfigFile type="ini"/>
</configsDirectory>
<bootstrapFile filesystemName="Bootstrap.php"/>
</applicationDirectory>
</projectDirectory>
</projectProfile>
Otherwise you have to twiggle on each of the nodes using the "filesystemName" attribute.
Doctrine and Zend Framework Integration Conventions
You have to follow one convention for ZFDoctrine to allow modular Doctrine models to work:
*Models have to be called Model for all modules including the module that is considered to be the "default" for the MVC.
Examples:
- Default_Model_User
- Default_Model_Group
- Blog_Model_Post
- Blog_Model_Category
To have Doctrine work with this classes you must explicitly spill out
all the refClass
, model
, local
and foreign
properties on the
Relations of your Doctrine models.
Doctrine-Enable a Zend Framework Project
To use tooling support, you have to Doctrine-Enable your Zend Framework Project by calling:
zf create-project doctrine --dsn=mysql://root:passwd@localhost/my_app --zend-project-style
This enables the Doctrine Zend_Application resource with the given DSN to connecto to a database.
It also enables some additional lines for autoloading "Doctrine" and "ZFDoctrine" namespaces
by the Zend_Loader_Autoloader
.
Additionally a project style has to be specified that defines where code-generation puts your entities.
After execution there are 4 new directories generated inside your application/configs directory:
application/configs/schema
- Contains the YAML Schema Metadataapplication/configs/migrations
- Contains the migration classesapplication/configs/fixtures
- Contains data-fixtures for your applicationapplication/configs/sql
- Contains SQL files
There are three different project styles that make sense in a Zend Framework Project:
- Zend Project Style with
--zend-project-style
is also the default. - One Library directory per Module
--library-per-module
- Single Library directory per project
--single-library
You can change this option at a later stage of the project, however you need to re-generate all the classes and manually delete the orphaned classes in this case.
You find this option in the application.ini under the section:
resources.doctrine.manager.attributes.attr_model_loading = "model_loading_zend"
Note
If Doctrine is not in your library or in your Include path you have to add it to the application.ini
includePaths.library = APPLICATION_PATH "/../library:/home/benny/code/doctrine1/lib"
1. Zend Project Style
The default Zend Project style has a folder "models" in each module. For example having only the default module your directories look like:
.
|-- application
| |-- Bootstrap.php
| |-- configs
| | |-- application.ini
| | |-- fixtures
| | |-- migrations
| | |-- schema
| | `-- sql
| |-- controllers
| | |-- ErrorController.php
| | `-- IndexController.php
| |-- *models*
| `-- views
| |-- helpers
| `-- scripts
| |-- error
| | `-- error.phtml
| `-- index
| `-- index.phtml
|-- library
|-- public
`-- index.php
Having two additional modules "blog" and "guestbook" we end up with:
.
|-- application
| |-- Bootstrap.php
| |-- configs
| | `-- application.ini
| |-- controllers
| | |-- ErrorController.php
| | `-- IndexController.php
| |-- *models*
| |-- modules
| | |-- blog
| | | |-- controllers
| | | |-- *models*
| | | `-- views
| | | |-- filters
| | | |-- helpers
| | | `-- scripts
| | `-- guestbook
| | |-- controllers
| | |-- *models*
| | `-- views
| | |-- filters
| | |-- helpers
| | `-- scripts
| `-- views
| |-- helpers
| `-- scripts
| |-- error
| | `-- error.phtml
| `-- index
| `-- index.phtml
|-- library
|-- public
`-- index.php
This approach requires you to use the Module Autoloaders, which I personally find slow and unintuitive.
application.ini
cofig is:
resources.doctrine.manager.attributes.attr_model_loading = "model_loading_zend"
2. Library per Module
In library per module you get some kind of duplication of knowledge in the paths, however you get to keep the simple Class Underscore to Directory Separator convention.
.
|-- application
| |-- Bootstrap.php
| |-- configs
| | `-- application.ini
| |-- controllers
| | |-- ErrorController.php
| | `-- IndexController.php
| |-- models
| |-- modules
| | |-- blog
| | | |-- controllers
| | | |-- library
| | | | `-- Blog
| | | | `-- Model
| | | `-- views
| | | |-- filters
| | | |-- helpers
| | | `-- scripts
| | `-- guestbook
| | |-- controllers
| | |-- library
| | | `-- Guestbook
| | | `-- Model
| | `-- views
| | |-- filters
| | |-- helpers
| | `-- scripts
| `-- views
| |-- helpers
| `-- scripts
| |-- error
| | `-- error.phtml
| `-- index
| `-- index.phtml
|-- library
|-- public
`-- index.php
Option is:
resources.doctrine.manager.attributes.attr_model_loading = "model_loading_zend_module_library"
3. Single Library Path
I have often seen the approach taken with a single library that contains all the models in subdirectories of each Modules library directory.
.
|-- application
| |-- Bootstrap.php
| |-- configs
| | `-- application.ini
| |-- controllers
| | |-- ErrorController.php
| | `-- IndexController.php
| |-- modules
| | |-- blog
| | | |-- controllers
| | | `-- views
| | | |-- filters
| | | |-- helpers
| | | `-- scripts
| | `-- guestbook
| | |-- controllers
| | `-- views
| | |-- filters
| | |-- helpers
| | `-- scripts
| `-- views
| |-- helpers
| `-- scripts
| |-- error
| | `-- error.phtml
| `-- index
| `-- index.phtml
|-- library
| |-- Blog
| | `-- Model
| |-- Guestbook
| | `-- Model
| `-- Model
|-- public
| `-- index.php
Option is:
resources.doctrine.manager.attributes.attr_model_loading = "model_loading_zend_single_library"
Generating Models (With a single Module)
Using Zend Doctrine Integration with a Single module first begs the question, what is that modules prefix? By
default, the prefix is "Default_Model_*". You can change this easily by opening up your application.ini
and
change it to "Zfplanet" for example like in our example, rebuilding Padraic Bradys
fine application ZFPlanet.
; appnamespace is just for automatically adding autoloading
appnamespace = "Zfplanet"
; defaultModule is used for for class generation
resources.frontController.defaultModule = "zfplanet"
Note
Make sure the defaultModule line is before the controllerDirectory line. Sadly the ordering is important in this one.
Grab the modified zfplanet.yml
file from the examples/Zfplanet folder of this project and put it in your projects
application/configs/schema/
folder, then run:
zf generate-models-from-yaml doctrine
And you should see lots of newly created model files in the applications/models
folder. Nice huh?
benny@benny-pc:~/code/php/zfdoctrine/singlemodule$ zf generate-models-from-yaml doctrine
[Doctrine] Generated record 'Zfplanet_Model_Blog' in Module 'zfplanet'.
[Doctrine] Generated record 'Zfplanet_Model_Feed' in Module 'zfplanet'.
[Doctrine] Generated record 'Zfplanet_Model_FeedMeta' in Module 'zfplanet'.
[Doctrine] Generated record 'Zfplanet_Model_Entry' in Module 'zfplanet'.
[Doctrine] Generated record 'Zfplanet_Model_Subscription' in Module 'zfplanet'.
[Doctrine] Generated record 'Zfplanet_Model_User' in Module 'zfplanet'.
[Doctrine] Successfully generated 6 record classes.
Generating Table Classes
If you want to generate the Table classes also you have to modify the application.ini to include:
resources.doctrine.generateModels.generateTableClasses = true
Generating Models in a Modular MVC
As a modular example we combine the "Real World Example" code from the Doctrine manual into two modules. We will implement a user-management and a forum in two modules. Our default module contains the user-management and our second module contains the forum code. Here is the modified YAML.
Note
Note the Default_Model_ and Forum_Model_ prefixes here which are necessary to comply with Zend Framework standards.
First the application/configs/schema/user.yml
:
---
Default_Model_User:
columns:
username: string(255)
password: string(255)
relations:
Roles:
class: Default_Model_Role
refClass: Default_Model_UserRole
foreignAlias: Users
local: user_id
foreign: role_id
Permissions:
class: Default_Model_Permission
refClass: Default_Model_UserPermission
foreignAlias: Users
local: user_id
foreign: permission_id
Default_Model_Role:
columns:
name: string(255)
relations:
Permissions:
class: Default_Model_Permission
refClass: Default_Model_RolePermission
foreignAlias: Roles
local: role_id
foreign: permission_id
Default_Model_Permission:
columns:
name: string(255)
Default_Model_RolePermission:
columns:
role_id:
type: integer
primary: true
permission_id:
type: integer
primary: true
relations:
Role:
class: Default_Model_Role
local: role_id
Permission:
class: Default_Model_Permission
local: permission_id
Default_Model_UserRole:
columns:
user_id:
type: integer
primary: true
role_id:
type: integer
primary: true
relations:
User:
class: Default_Model_User
local: user_id
Role:
class: Default_Model_Role
local: role_id
Default_Model_UserPermission:
columns:
user_id:
type: integer
primary: true
permission_id:
type: integer
primary: true
relations:
User:
class: Default_Model_User
local: user_id
Permission:
class: Default_Model_Role
local: permission_id
And the second file put into application/configs/schema/forum.yml
:
---
Forum_Model_Category:
columns:
root_category_id: integer(10)
parent_category_id: integer(10)
name: string(50)
description: string(99999)
relations:
Subcategory:
class: Forum_Model_Category
local: parent_category_id
foreign: id
Rootcategory:
class: Forum_Model_Category
local: root_category_id
foreign: id
Forum_Model_Board:
columns:
category_id: integer(10)
name: string(100)
description: string(5000)
relations:
Category:
class: Forum_Model_Category
local: category_id
foreign: id
Threads:
class: Forum_Model_Thread
local: id
foreign: board_id
Forum_Model_Entry:
columns:
topic: string(100)
message: string(99999)
parent_entry_id: integer(10)
thread_id: integer(10)
date: integer(10)
relations:
Author:
class: Model_User
local: author_id
foreign: id
Parent:
class: Forum_Model_Entry
local: parent_entry_id
foreign: id
Thread:
class: Forum_Model_Thread
local: thread_id
foreign: id
Forum_Model_Thread:
columns:
board_id: integer(10)
updated: integer(10)
closed: integer(1)
relations:
Board:
class: Forum_Model_Board
local: board_id
foreign: id
Entries:
class: Forum_Model_Entry
local: id
foreign: thread_id
Now if we put both files into application/configs/schema
we can generate the model classes:
zf generate-models-from-yaml doctrine
We get an error:
An Error Has Occurred
Unknown Zend Controller Module 'forum' inflected from model class
'Forum_Model_Category'. Have you configured your front-controller to
include modules?
We have to generate the modules:
zf create module default
zf create module forum
Now edit the application.ini and replace the entry resources.frontcontroller.controllerDirectory
with:
resources.frontcontroller.moduleDirectory = APPLICATION_PATH "/modules"
For the "Zend Project Style" using the models/ directories we now need to instantiate module loaders, here is a generic approach to this problem:
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
public function _initModuleLoaders()
{
$this->bootstrap('Frontcontroller');
$fc = $this->getResource('Frontcontroller');
$modules = $fc->getControllerDirectory();
foreach ($modules AS $module => $dir) {
$moduleName = strtolower($module);
$moduleName = str_replace(array('-', '.'), ' ', $moduleName);
$moduleName = ucwords($moduleName);
$moduleName = str_replace(' ', '', $moduleName);
$loader = new Zend_Application_Module_Autoloader(array(
'namespace' => $moduleName,
'basePath' => realpath($dir . "/../"),
));
}
}
}
For the other two project styles you have to make sure that the Zend_Loader_Autoloader
or any
other PSR-0 compatible loader points to the library
directories correctly.
Generating the Database Tables
Now that we have an existing model we can create the database from it calling:
zf build-project doctrine --reload
This drops the database, re-creates it with tables and loads possible fixture
data located in the applications/config/fixtures
.
Using Migrations
Doctrine 1 comes with a powerful migration support, which can also accessed from the Zend Tool providers.
You can show the current migration version:
zf show-migration doctrine
You can generate new migrations either by creating a bare class:
zf generate-migration doctrine --class-name=MyMigration
From a database-diff:
zf generate-migration doctrine --from-database
From a model diff:
zf generate-migration doctrine --from-models
To migrate your database you can either specify a version to migrate to:
zf excecute-migration doctrine --to-version=123
Or don't specify a version and migrate to the latest:
zf excecute-migration doctrine
Paginator Adapter
ZFDoctrine offers a pagination adapter for Zend_Paginator
that makes Doctrine_Query
instances
paginateable:
$query = Doctrine_Query::create(...)
$adapter = new ZFDoctrine_Paginator_Adapter($query);
$paginator = new Zend_Paginator($adapter);
Dynamic Form Generation
For admin purposes its often necesary and cumbersome to implement lots of forms that
allow to add or edit database records. Using the ZFDoctrine_Form
component you can
easily generate simple and more complex forms that directly work on Doctrine_Record
instances. These forms are dynamically generated upon request.
The very simplest form would look like this:
$form = new ZFDoctrine_From_Model(array(
'model' => 'Default_Model_User',
'action' => '.',
'method' => 'post'
));
This would give you a class with the same interface as Zend_Form, but it would automatically
get fields from the model called Default_Model_User
. Having the same interface as Zend_Form means that
you can essentially use the class just like any other Zend_Form instance; it just has some more magic. :)
Basic usage: Creating new records
//ExampleController.php
class ExampleController extends Zend_Controller_Action
{
public function createAction()
{
$form = new ZFDoctrine_Form_Model(array(
'model' => 'Default_Model_User',
'action' => '.',
'method' => 'post'
));
if($this->getRequest()->isPost() && $form->isValid($_POST)) {
// This saves the form's data to the DB.
// $record will be the new model instance created when saving.
$record = $form->save();
//redirect elsewhere after completion
$this->_helper->redirect('update', 'example');
}
//assign the form to the view
$this->view->form = $form;
}
}
The view script for formAction
looks plain simple:
<h2>Fill this form</h2>
<?= $this->form; ?>
The formAction method is a pretty typical form process action: it creates a form and validates it if the request was a form submission, or displays it if it wasn’t or something was not valid.
Editing existing records
Editing existing records is similar to what seen above, but we must first load a record and assign it to the form before rendering, validating or saving:
public function editUserAction()
{
$userId = $this->_getParam('id');
$form = new ZFDoctrine_Form_Model(array(
'model' => 'Default_Model_User',
'action' => '/zf-doctrine/index/edit-user/id/'.$userId,
'method' => 'post',
));
$user = Doctrine::getTable('Default_Model_User')->find($userId);
$form->setRecord($user);
if($this->getRequest()->isPost() && $form->isValid($_POST)) {
$record = $form->save();
// redirect elsewhere after completion
$this->_redirect('/');
}
//assign the form to the view
$this->view->form = $userId;
$this->view->form = $form;
}
After using setRecord, the default field values will be read from the record passed to the method. Also, when calling save(), any modifications will be saved to this record instead of a new one.
ModelForm Action Helper
The model form action helper simplifies the CRUD workflow of your application considerably, it helps you abstract to write the create and update form controller actions if you obey to some conventions:
- The Helper uses the
$form
,$recordId
and$record
view variables, don't reuse them in an action using the helper. - The Helper assumes GET and POST handling of a form are done in the same action method.
- The helper automatically sets the form method to POST and the form action to the current MVC action.
Register the ModelForm action helper by calling the following somewhere in your bootstrap:
Zend_Controller_Action_HelperBroker::addPath('ZFDoctrine/Controller/Helper', 'ZFDoctrine_Controller_Helper');
See it in action for a Create form, the Controller Action:
public function addCategoryAction()
{
$form = $this->createCategoryForm();
$this->_helper->modelForm($form, "index");
}
And the view script add-category.phtml
:
<h1>Create a Category</h1>
<?= $this->form; ?>
Using the model form helper for updating a Doctrine Record is slightly more complicated, the controller action looks like:
public function editCategoryAction()
{
$form = $this->createCategoryForm();
$this->getHelper('modelForm')
->setRecordIdParam('id')
->handleForm($form, 'index');
}
And the view script edit-category.pthml
:
<h1>Edit Category: <?= $this->record->name; ?><h1>
<?= $this->form; ?>
The method setRecordIdParam()
accepts the name of the GET variable that contains
the id of a record. If the value of this field is NULL
the form creates a new
record instance, if the value of this field is not null but no record is found
of this model with that particular id an exception of the type ZFDoctrine_DoctrineException
is thrown.
Handling Relations
All to one relations and all to many relations are handled by select and multi-select html fields
respectively. By default they render as their ID, however when you implement the __toString()
method
on your Doctrine records you can modify how they are displayed in the forms.
Modifying the form behavior
The class in its current implementation supports both creating new records and editing existing ones, and it can also display some relations as select boxes and subforms. You can also make it ignore chosen columns so it won’t autogenerate fields for them and such. Other options include giving labels for fields and switching their types. Advanced settings example
<?php
class AdvancedForm extends ZFDoctrine_Form_Model
{
protected $_model = 'Article';
// By default, many-relations will be generated, but you can disable them.
protected $_generateManyFields = true;
// Let's ignore these two fields
protected $_ignoreFields = array('created_at', 'updated_at');
// Make the content column's field type 'textarea' instead of the default 'text'
protected $_fieldTypes = array(
'content' => 'textarea'
);
// Give some human-friendly labels for the fields:
protected $_fieldLabels = array(
'name' => 'Article name',
'content' => 'Article content',
'category_id' => 'Category'
);
protected function _preGenerate()
{
// this method is called before the form is generated
}
protected function _postGenerate()
{
// this method is called after the form is generated
}
}
In the above snippet you can see all the configuration options and the two event methods. Most of the variables should be quite self-explanatory.