Utilizando PHP 7, MongoDB e Doctrine
de Oliveira, Lucas • September 18, 2016
php php7 mongodb doctrineFaz um tempo que estou utilizando PHP com MongoDB e faz um tempo também que vi que tinha uma versão do Doctrine que poderia me ajudar com isso, porém quando tinha visto (ano passado) e fui fazer uns testes tive alguns problemas por ainda estar na versão BETA, esses dias resolvi olhar novamente e vi que já faz alguns meses que a versão 1.0 foi lançada e que está até na versão 1.1, então decidi fazer uns testes novamente e compartilhar aqui no blog.
Nesse post vou dar uma introdução de como trabalhar com PHP e MongoDB utilizando o framework de persistência Doctrine ODM. Como exemplo, vou desenvolver um blog simples com posts e comentários, estarei mostrando aqui apenas os códigos da API que estão relacionados com o Doctrine, porém todo o código da aplicação estará disponível e um repositório no GitHub com um cliente Ajax consumindo a API. Vou utilizar também algumas ferramentas a mais apenas para agilizar o desenvolvimento, como, o micro-framework Silex e o tamplate engine Twig, mas o mesmo procedimento poderá ser facilmente implementado em outros frameworks ou até mesmo em aplicações que utilizam "apenas PHP".
Sendo assim, o blog deverá ter os seguintes recursos na API:
- [POST] /api/post // Inserir post
- [GET] /api/post // Listar dados os posts
- [GET] /api/post/{id} // Retornar um post
- [POST] /api/post/{id}/comment // Inserir comentário no post
- Obs: todos os posts e comentários deverão ser retornados em ordem decrescente por data de criação.
Primeiro, vamos entender um pouco o que é o Doctrine ODM. O Doctrine ODM (Object Document Mapper) é um framework que foi desenvolvido para o PHP 5.3.0+ e provê uma forma mais transparente de persistir objetos do PHP no MongoDB. Quem já utilizou o Doctrine ORM (Object Relational Mapper), pode ter uma facilidade maior para entender a versão para trabalhar com MongoDB, porém é importante também ter conhecimento especificamente sobre o banco de dados para que se possa utilizar da melhor forma tanto o framework quanto as vantagens que o MongoDB oferece. Caso não conheça e tenha interesse em aprender sobre MongoDB, atualmente existem vários cursos na internet que podem ajudar, porém dois dos que eu fiz, que são gratuitos e que me ajudaram muito, foi o da Webschool e o da MongoDB University.
Antes de iniciar o desenvolvimento é necessário ter as seguintes ferramentas instaladas:
- PHP - Minha versão: 7.1.0
- Composer - Minha versão: 1.1.2
- MongoDB - Minha versão: 3.0.7 / testei também na versão 3.2.9
Logo em seguida é preciso baixar e configurar o Driver do MongoDB para PHP:
No Windows o download da extensão pode ser feito no site do pecl, depois de ter feito o download, o arquivo
.dll
deve ser adicionado na pastaext
do PHP e a linhaextension=php_mongodb.dll
deve ser adicionada nophp.ini
.No Linux o driver pode ser instalado utilizando o
pecl
, conforme o descrito no repositório do mongo-php-library.
Para verificar se a extensão está instalada, digite o comando php -m
no terminal para mostrar a lista de extensões habilitadas, nessa lista deve aparecer mongodb
.
Com as ferramentas instaladas e o driver configurado, podemos criar a pasta para iniciar o projeto, no meu caso chamarei de doctrine-odm-blog
, feito isso, é necessário criar o arquivo composer.json para instalar as dependências. O arquivo deverá ficar parecido o JSON a seguir.
{
"name": "deoliveiralucas/doctrine-odm-blog",
"description": "Blog simples com Doctrine ODM e Silex",
"require": {
"alcaeus/mongo-php-adapter": "^1.0",
"doctrine/mongodb": "^1.3",
"doctrine/mongodb-odm": "^1.1",
"silex/silex": "^2.0",
"twig/twig": "~1.0"
},
"autoload": {
"psr-4": {
"DOLucas\\": "src/"
}
},
"authors": [
{
"name": "Lucas de Oliveira",
"email": "contato@deoliveiralucas.net"
}
]
}
Note que adicionei também a biblioteca alcaeus/mongo-php-adapter
, pelo fato da versão atual do Doctrine ODM (1.3) não suportar a nova versão da extensão do MongoDB para PHP é necessário a instalação desse adapter para que o framework funcione normalmente.
Agora posso instalar as dependências, talvez seja necessário utilizar o parâmetro --ignore-platform-reqs
, pois justamente por essa incompatibilidade o Composer pode barrar a instalação:
$ composer install --ignore-platform-reqs
Depois de instalado, criei mais algumas pastas e arquivos para que a estrutura fique conforme o lista a seguir, lembrando que o objetivo aqui será mostrar apenas os arquivos que estão na pasta Document
, Repository
e Service
.
|-public
|-assets
|-index.php
|-src
|-Blog
|-Controller
|-PostController.php
|-Document
|-Comment.php
|-Post.php
|-Factory
|-CommentFactory.php
|-PostFactory.php
|-Repository
|-PostRepository.php
|-Service
|-PostService.php
|-res
|-views
|-create.twig
|-index.twig
|-layout.twig
|-post.twig
|-vendor
|-bootstrap.php
|-composer.json
|-composer.lock
Agora vou adicionar o script no arquivo bootstrap.php
para criar a conexão com o banco de dados e configurar onde será salvo as classes de Proxies e Hydrators gerados pelo Doctrine, no meu caso deixarei a pasta /tmp
e o nome do banco doctrineblog
, caso não seja configurado o nome do banco utilizando o método $config->setDefaultDB
, por padrão será criado com o nome doctrine
.
<?php
require_once __DIR__ . '/vendor/autoload.php';
use Silex\Application;
use Silex\Provider\TwigServiceProvider;
use Doctrine\MongoDB\Connection;
use Doctrine\ODM\MongoDB\Configuration;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver;
$app = new Application();
$app['debug'] = true;
$app->register(new TwigServiceProvider(), [
'twig.path' => __DIR__ . '/res/views',
]);
// Início da configuração do Doctrine
AnnotationDriver::registerAnnotationClasses();
$config = new Configuration();
$config->setProxyDir('/tmp');
$config->setProxyNamespace('Proxies');
$config->setHydratorDir('/tmp');
$config->setHydratorNamespace('Hydrators');
$config->setMetadataDriverImpl(AnnotationDriver::create('/tmp'));
$config->setDefaultDB('doctrineblog');
$dm = DocumentManager::create(new Connection(), $config);
No caso acima, a conexão está sendo feita com um banco de dados local, para conectar com um banco remoto, a linha para criar o DocumentManager
deve estar da seguinte forma:
<?php
$server = "mongodb://user:pass@server:port/dbname";
$dm = DocumentManager::create(new Connection($server), $config);
Depois de ter criado a conexão, vou criar os arquivos responsáveis por mapear os objetos da minha aplicação com as coleções do banco de dados, assim como no Doctrine ORM é possível fazer isso de forma simples utilizando as anotações, XML ou YAML, nesse exemplo estarei utilizando anotações. Para que as anotações funcionem é necessário que importe a classe Annotations
(no caso abaixo foi dado um apelido ODM
) e depois informe ao Doctrine que a classe que está sendo criada é um documento do MongoDB com a anotação @ODM\Document
, posso informar também qual o nome da coleção que deverá ser armazenado esse documento passando como parâmetro o nome da coleção @ODM\Document(collection="posts")
, caso não informe o nome da coleção, ele será criado com o mesmo nome da classe, em seguida posso criar os campos do documento utilizando as anotações.
Como um post deve ter vários comentários é importante destacar o atributo $comments
onde está dizendo que deverá referenciar vários documentos (ReferenceMany
) utilizando a estratégia (strategy
) set
que é um comando do MongoDB, o documento alvo (targetDocument
) que será referenciado dentro dessa lista é o Comment
(uma classe que deverá ser criada), qualquer operação executada no documento pai (Post
) poderá refletir nos documentos filhos (Comment
) utilizando o parâmetro cascade="all"
(esse opção pode ser personalizada) e por fim, conforme descrito no início do post os comentários retornados deverão estar em ordem decrescente pela data de criação, para isso usamos o parâmetro sort={"createdAt": "desc"}
.
Sendo assim, a classe Post
deve ficar da seguinte forma:
<?php
namespace DOLucas\Blog\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use DateTime;
/**
* @ODM\Document(collection="posts")
*/
class Post
{
/**
* @ODM\Id
*/
private $id;
/**
* @ODM\Field(type="string")
*/
private $title;
/**
* @ODM\Field(type="string")
*/
private $body;
/**
* @ODM\Field(type="string")
*/
private $author;
/**
* @ODM\ReferenceMany(
* strategy="set",
* targetDocument="Comment",
* cascade="all",
* sort={"createdAt": "desc"}
* )
*/
private $comments = [];
/**
* @ODM\Field(type="date")
*/
private $createdAt;
/**
* @param string $title
* @param string $body
* @param string $author
*/
public function __construct(string $title, string $body, string $author)
{
$this->setTitle($title);
$this->setBody($body);
$this->setAuthor($author);
$this->setCreatedAt(new DateTime());
}
/**
* @return string
*/
public function getId() : string
{
return $this->id;
}
/**
* @param string $title
*/
private function setTitle(string $title)
{
$this->title = $title;
}
/**
* @return string
*/
public function getTitle() : string
{
return $this->title;
}
/**
* @param string $body
*/
private function setBody(string $body)
{
$this->body = $body;
}
/**
* @return string
*/
public function getBody() : string
{
return $this->body;
}
/**
* @param string $author
*/
private function setAuthor(string $author)
{
$this->author = $author;
}
/**
* @return string
*/
public function getAuthor() : string
{
return $this->author;
}
/**
* @param Comment $comment
*/
public function addComment(Comment $comment)
{
$this->comments[] = $comment;
}
/**
* @return array
*/
public function getComments() : array
{
return $this->comments->toArray();
}
/**
* @param DateTime
*/
private function setCreatedAt(DateTime $createdAt)
{
$this->createdAt = $createdAt;
}
/**
* @return DateTime
*/
public function getCreatedAt() : DateTime
{
return $this->createdAt;
}
}
Depois vou mapear a classe Comment
:
<?php
namespace DOLucas\Blog\Document;
use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM;
use DateTime;
/**
* @ODM\Document(collection="comments")
*/
class Comment
{
/**
* @ODM\Id
*/
private $id;
/**
* @ODM\Field(type="string")
*/
private $username;
/**
* @ODM\Field(type="string")
*/
private $comment;
/**
* @ODM\Field(type="date")
*/
private $createdAt;
/**
* @return string
*/
public function getId() : string
{
return $this->id;
}
/**
* @param string $title
*/
public function setTitle(string $title)
{
$this->title = $title;
}
/**
* @param string $username
*/
public function setUsername(string $username)
{
$this->username = $username;
}
/**
* @return string
*/
public function getUsername() : string
{
return $this->username;
}
/**
* @param string $comment
*/
public function setComment(string $comment)
{
$this->comment = $comment;
}
/**
* @return string
*/
public function getComment() : string
{
return $this->comment;
}
/**
* @param DateTime
*/
public function setCreatedAt(DateTime $createdAt)
{
$this->createdAt = $createdAt;
}
/**
* @return DateTime
*/
public function getCreatedAt() : DateTime
{
return $this->createdAt;
}
}
Agora que temos nossas classes mapeadas com o banco de dados já é possível utilizá-las, para isso poderíamos fazer da seguinte forma para armazenar e retornar os posts do banco de dados, utilizando o $dm
que é o DocumentManager criado no arquivo bootstrap.php
:
<?php
// criar o post
$post = new Post('Titulo do Post', 'Descrição do post', '@deoliveiralucas');
// avisa ao Doctrine para armazenar o post no próximo flush()
$dm->persist($post);
// amazena post
$dm->flush();
// para retornar os posts
$posts = $this->dm->getRepository(Post::class)->findAll();
Mas para que fique algo mais organizado, criei uma classe chamada PostRepostory
que recebe o DocumentManager no construtor. Para realizar as consultas vou utilizar a Query Builder API que possibilita criar consultas mais personalizadas, pois será necessário retornar o posts em ordem decrescente por data de criação.
Então, a classe PostRepository
deve ficar da seguinte forma:
<?php
namespace DOLucas\Blog\Repository;
use DOLucas\Blog\Document\Post;
use Doctrine\ODM\MongoDB\DocumentManager;
use Doctrine\ODM\MongoDB\Cursor;
/**
* @author Lucas de Oliveira <contato@deoliveiralucas.net>
*/
class PostRepository
{
/**
* @var DocumentManager
*/
private $dm;
/**
* @var \Doctrine\Common\Persistence\ObjectRepository
*/
private $queryBuilder;
/**
* @param DocumentManager $dm
*/
public function __construct(DocumentManager $dm)
{
$this->dm = $dm;
$this->queryBuilder = $dm->createQueryBuilder(Post::class);
}
/**
* @param Post $post
*/
public function save(Post $post)
{
$this->dm->persist($post);
$this->dm->flush();
}
/**
* @return Cursor
*/
public function getAll() : Cursor
{
return $this
->queryBuilder
->sort('createdAt', 'desc')
->getQuery()
->execute();
}
/**
* @param string $id
* @return Post
*/
public function getById(string $id) : Post
{
return $this
->queryBuilder
->field('_id')->equals($id)
->getQuery()
->getSingleResult();
}
}
Com a classe repository e os documents prontos, vou criar mais uma classe chamada PostService
que será responsável por inserir as informações que vem da requisição no banco de dados utilizando as classes criadas anteriormente e utilizar o método toArray
para mostrar os dados de forma mais limpa na API. Adicionei também as classes Factories que é responsável por instanciar os documents e os Validators. Então a classe PostService
com todos os métodos que precisamos, ficou da seguinte maneira:
<?php
namespace DOLucas\Blog\Service;
use DOLucas\Blog\Repository\PostRepository;
use DOLucas\Blog\Factory\PostFactory;
use DOLucas\Blog\Factory\CommentFactory;
use DOLucas\Blog\Document\Post;
use DOLucas\Blog\Validator\PostValidator;
use DOLucas\Blog\Validator\CommentValidator;
use Doctrine\ODM\MongoDB\Cursor;
use InvalidArgumentException as ArgumentException;
/**
* @author Lucas de Oliveira <contato@deoliveiralucas.net>
*/
class PostService
{
/**
* @var PostRepository
*/
private $repository;
/**
* @var PostFactory
*/
private $postFactory;
/**
* @var CommentFactory
*/
private $commentFactory;
/**
* @var PostValidator
*/
private $postValidator;
/**
* @var CommentValidator
*/
private $commentValidator;
/**
* @param PostRepository $postRepository
* @param PostFactory $postFactory
* @param CommentFactory $commentFactory
* @param PostValidator $postValidator
* @param CommentValidator $commentValidator
*/
public function __construct(
PostRepository $postRepository,
PostFactory $postFactory,
CommentFactory $commentFactory,
PostValidator $postValidator,
CommentValidator $commentValidator
) {
$this->repository = $postRepository;
$this->postFactory = $postFactory;
$this->commentFactory = $commentFactory;
$this->postValidator = $postValidator;
$this->commentValidator = $commentValidator;
}
/**
* @param array $params
*/
public function create(array $params) : array
{
try {
$this->postValidator->validate($params);
$post = $this->postFactory->create(
$params['title'],
$params['body'],
$params['author']
);
$this->repository->save($post);
return [
'status' => 'success',
'message' => 'Post inserido com sucesso'
];
} catch (ArgumentException $e) {
return [
'status' => 'error',
'message' => $e->getMessage()
];
} catch (Exception $e) {
return [
'status' => 'error',
'message' => 'Ocorreu um problema ao inserir novo post',
'internal' => $e->getMessage()
];
}
}
/**
* @return array
*/
public function getAll() : array
{
return $this->toArray($this->repository->getAll());
}
/**
* @param string $idPost
* @return array
*/
public function getById(string $idPost) : array
{
return $this->toArray($this->repository->getById($idPost));
}
/**
* @param string $idPost
* @param array $params
* @return array
*/
public function addComment(string $idPost, array $params) : array
{
try {
$this->commentValidator->validate($params);
$post = $this->repository->getById($idPost);
$comment = $this->commentFactory->create(
$params['username'],
$params['comment']
);
$post->addComment($comment);
$this->repository->save($post);
return [
'status' => 'success',
'message' => 'Comentário adicionado com sucesso'
];
} catch (ArgumentException $e) {
return [
'status' => 'error',
'message' => $e->getMessage()
];
} catch (Exception $e) {
return [
'status' => 'error',
'message' => 'Ocorreu um problema ao adicionar comentário',
'internal' => $e->getMessage()
];
}
}
/**
* @param array|Post $posts
* @return array
*/
public function toArray($posts) : array
{
$arrPost = [];
if ($posts instanceof Cursor) {
foreach ($posts as $post) {
$arrPost[] = $this->postToArray($post);
}
return $arrPost;
}
if ($posts instanceof Post) {
return $this->postToArray($posts);
}
return $arrPost;
}
/**
* @param Post $post
* @return array
*/
protected function postToArray(Post $post) : array
{
return [
'id' => $post->getId(),
'title' => $post->getTitle(),
'body' => $post->getBody(),
'author' => $post->getAuthor(),
'createdAt' => $post->getCreatedAt(),
'comments' => $this->commentsToArray($post->getComments())
];
}
/**
* @param array $comments
* @return array
*/
protected function commentsToArray(array $comments) : array
{
$arrComments = [];
foreach ($comments as $comment) {
$arrComments[] = [
'id' => $comment->getId(),
'username' => $comment->getUsername(),
'comment' => $comment->getComment(),
'createdAt' => $comment->getCreatedAt()
];
}
return $arrComments;
}
}
O código completo da aplicação está disponível nesse link: http://github.com/deoliveiralucas/blog-php-mongodb-doctrine.
Podemos ver a facilidade que o Doctrine nos oferece para trabalhar com MongoDB, mas vale ressaltar também que essa facilidade pode trazer uma pouco de perda performance quando comparado com a utilização apenas das classes da extensão do PHP, que também é simples de utilizar.
Eu particularmente já desenvolvi projetos que usam PHP com MongoDB, mas ainda não utilizei o Doctrine ODM pelo motivo de que quando fui pesquisar ainda estava na versão BETA e não quis arriscar, mas se atualmente eu fosse começar um novo projeto, com certeza daria uma atenção maior a esse framework.
A fonte principal desse post foi a documentação oficial do Doctrine ODM, caso queira saber mais recomendo que acesse, pois está bem completa: http://docs.doctrine-project.org/projects/doctrine-mongodb-odm/en/latest/.