Doctrine2 Extensions
, (*1)
This library extends Doctrine2 base classes with some useful things., (*2)
Installation
This library may be installed using Composer or by cloning it from its GitHub repository. These options are described below., (*3)
Composer, (*4)
You can read more about Composer and its main repository on
http://packagist.org. To install these doctrine extensions using Composer, first install Composer for your project using the instructions on the Packagist home page. You can then define your development dependency on doctrine-extensions using the parameters suggested below. While every effort is made to keep the master branch stable, you may prefer to use the current stable version tag instead., (*5)
{
"require-dev": {
"weavora/doctrine-extensions": "dev-master@dev"
}
}
To install, you may call:, (*6)
composer.phar install
Git / GitHub, (*7)
The git repository hosts the development version in its master branch. You can
install it using Composer by referencing dev-master as your preferred version
in your project's composer.json file as the previous example shows., (*8)
You may also install this development version:, (*9)
git clone git://github.com/weavora/doctrine-extensions.git
cd doctrine-extensions
The above processes will install library to the doctrine-extensions folder., (*10)
ORM Extensions
Doctrine advices to use entity repositories that will contain business logic related to entities retrieval.
Most probably, you will use a query builder inside the repository to build DQL.
The issue is that a standard query builder has quite general API and doesn't provide such useful shortcuts like named scopes, a single method to apply custom criteria with parameters, etc., (*11)
This library is intended to fix what's missing and make your life a little bit easier., (*12)
How to organize repositories
Let's say you want to create a blog application. You'll probably create a post entity which will have references to the category, author and comments. And now you're thinking about an approach to organize your repositories., (*13)
Common issues with repositories:, (*14)
- You have to duplicate code as soon as you have duplicated conditions
- Even simple methods with few conditions and ordering look massive
You can solve the first issue with a custom query builder per entity. It will also hide your criteria details from the repository that doesn't really need to know that., (*15)
To solve the issue with huge and not very descriptive methods, you'll create an extension for the query builder with some helpful shortcuts to make code more readable., (*16)
An example of how PostQueryBuilder & PostRepository could look like:, (*17)
<?php
namespace Acme\BlogBundle\Entity;
use Weavora\Doctrine\ORM as ORM;
/**
* Custom query class for Post entity
* Contains useful criteria set for posts filtering
*/
class PostQueryBuilder extends ORM\EntityQueryBuilder
{
public function published()
{
return $this->filterByColumn('Post.publishStatus', Post::STATUS_PUBLISHED);
}
public function recentFirst()
{
return $this->orderBy('Post.publishedAt', 'DESC');
}
}
/**
* Post entity repository
* Contains methods for fetch posts
*/
class PostRepository extends ORM\EntityRepository
{
/**
* Instantiate custom query builder
* @return PostQueryBuilder
*/
public function filter()
{
return new PostQueryBuilder($this->getEntityManager(), $this);
}
/**
* Find 10 recent posts
*
* @return Post[]
*/
public function findRecent()
{
return $this
->filter() // use PostQueryBuilder
->published() // get only published posts
->limit(10) // get only first 10 posts
->recentFirst() // most recent posts should go first
->fetchAll(); // get posts
}
/**
* Find posts by category
*
* @param Category $category
* @param int $page
* @param int $itemsPerPage
* @return Post[]
*/
public function findByCategory(Category $category, $page = 1, $itemsPerPage = 10)
{
return $this
->filter() // use PostQueryBuilder
->filterByColumn('Post.category', $category) // get only posts in the specified category
->paginate($page, $itemsPerPage) // get only the specified page
->recentFirst() // most recent posts should go first
->fetchAll(); // get posts
}
/**
* Count posts by author
*
* @param Author $author
* @return int
*/
public function countByAuthor(Author $author)
{
return $this
->filter() // use PostQueryBuilder
->select('COUNT(Post.id)') // calculate count
->filterByColumn('Post.author', $author) // calculate only author's posts
->groupBy('Post.author') // group by author
->fetchScalar(); // get scalar results (the first column of the first row)
}
}
Quite simple, right?, (*18)
API / EntityQueryBuilder
getEntityAlias() : string | Get entity alias in the query, (*19)
// entity class name -> alias
// \Entity\Post -> Post
// \Acme\DemoBundle\Entity\AuthorSettings -> AuthorSettings
// \Comment -> Comment
$alias = $queryBuilder->getEntityAlias();
filterByColumn($columnName, $value, $strict = true) : EntityQueryBuilder | Compare a column with the specified value, (*20)
// SELECT * FROM Entity\Post Post WHERE Post.title = :p1, [p1 = 'Post 1']
$queryBuilder->filterByColumn('Post.title', 'Post 1');
// SELECT * FROM Entity\Post Post WHERE Post.category IN (1,2,3)
$queryBuilder->filterByColumn('Post.category', array(1,2,3));
// SELECT * FROM Entity\Post Post WHERE Post.author IS NULL
$queryBuilder->filterByColumn('Post.author', null);
// SELECT * FROM Entity\Post Post
$queryBuilder->filterByColumn('Post.author', null, false);
// SELECT * FROM Entity\Post Post WHERE Post.author = 1
$queryBuilder->filterByColumn('Post.author', 1, false);
filterByStatement($statement, $parameters = array()) : EntityQueryBuilder | Add a custom statement, (*21)
// SELECT * FROM Entity\Post Post WHERE Post.title = :title, [title = 'Post 1']
$queryBuilder->filterByStatement('Post.title = :title', ['title' => 'Post 1']);
// SELECT * FROM Entity\Post Post WHERE Post.category IN (1,2,3)
$queryBuilder->filterByColumn('Post.category IN (1,2,3)');
limit($maxResults, $offset = null) : EntityQueryBuilder | Limit results, (*22)
// SELECT * FROM Entity\Post Post LIMIT 0, 10
$queryBuilder->limit(10);
// SELECT * FROM Entity\Post Post LIMIT 15, 10
$queryBuilder->limit(10, 15);
fetchAll($parameters = array()) : EntityClass[] | Fetch a result, (*23)
// SELECT * FROM Entity\Post Post -> Post[]
$queryBuilder->fetchAll();
// SELECT * FROM Entity\Post Post LIMIT 0, 10 -> Post[]
$queryBuilder->filterByStatement('Post.title = :title')->fetchAll(['title' => 'Post 1']);
fetchOne($parameters = array()) : EntityClass | Fetch the first result, (*24)
// SELECT * FROM Entity\Post Post LIMIT 0, 1 -> Post
$queryBuilder->fetchOne();
fetchScalar($parameters = array()) : int|string|float|null | Fetch a scalar result, (*25)
// SELECT COUNT(*) FROM Entity\Post Post LIMIT 0, 1 -> int
$queryBuilder->select('COUNT(*)')->fetchScalar();
DBAL Extensions
There is only one small enhancement to DBAL classes - Connection::lockSafeUpdate
that allows you to restart a query in case a transaction has been locked and failed. Maybe, it will be useful to show how to extend Doctrine connection class with custom methods., (*26)
How to configure, (*27)
# config.yml
doctrine:
dbal:
wrapper_class: 'Weavora\Doctrine\DBAL\Connection'
Usage example, (*28)
// Method will retry a query if it failed because of lock the first time
// You can specify the retry number as the 3rd argument
$doctrine->getConnection()->locksSafeUpdate("UPDATE posts SET category_id = :category", ['category' => 2]);
About
Stability
It's not stable yet. Please, use it at your own risk., (*29)
Requirements
- Any flavor of PHP 5.3 or later should do
- [optional] PHPUnit 3.5+ to execute the test suite (phpunit --version)
Submitting bugs and feature requests
Bugs and feature request are tracked on GitHub, (*30)
Author
Weavora LLC - http://weavora.com - http://twitter.com/weavora
Also see the list of contributors who have participated in this project., (*31)
License
This library is licensed under the MIT License - see the LICENSE
file for details, (*32)