let say that I have something like this:
class User{
/**
* Object that is lazy loaded
*/
private $statistic; //object with some stored data and some calculated data
}
Some of the $statistic's properties are stored in the DB but some other of them are calculated by analyzing the user activity (querying data records).
the thing is that I got a $user and when I run $user->getStatistic() as spected, I get the stored $statistic data and I need to add more data using sql queries and I don't know where to program this functionality.
¿overriding the Repository? I try overriding the find() method but it doesn't work
I know that if I use the active record pattern this can be done with no problem giving that I can access the DB in the construct method or the getters maybe, etc.
but I don't know how this could be done with doctrine standard behavior.
I believe that there must be a way to ensure that every instance of the Statistic Class have this calculated data on it.
I'm using symfony... maybe a service or something...
There are a number of ways to solve your problem.
Doctrine listener
This is probably the easiest one. Use Doctrine postLoad event to fill out data you need on your User model.
This is a completely valid approach, but has a couple of drawbacks:
This will be ran every time doctrine fetches an User entity instance from database. If it fetches a list of 100 users, it will be ran 100 times. This could make a performance problem if you do some time-consuming tasks in there.
They are hard to debug: if an error is encountered, events usually make code flow a lot less clear and therefore make debugging harder. If you do simple stuff, and don't overuse them, then it's probably fine, otherwise think about other options.
Abstracting away doctrine
I'm strongly in favor of this approach and I use it in almost every project.
Even though I'm one of the people who try to have the least amount of layers and indirection necessary, I do think that wrapping data persistence into your own services is a good idea. It isolates rest of your system from having to know how your data is stored.
I suggest not using Doctrine repositories/Entity manager directly. Instead, wrap them in your own services.
This makes your persistence API squeaky clean and obvious, while giving you ability to manipulate your models before they reach your business logic.
Here is an example of how I would approach your problem:
# src/AppBundle/Repository/UserRepository.php
class UserRepository
{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
public function findById($userId)
{
$user = $this->em->getRepository(User::class)->find($userId);
$this->calculateUserStatistics($user);
return $user;
}
public function save(User $user)
{
$this->em->persist($user);
$this->em->flush();
}
// ...
private function calculateUserStatistics(User $user)
{
// calculate and set statistics on user object
}
}
This approach has a number of advantages:
Decoupling from Doctrine
Your business code is no longer coupled to Doctrine, it doesn't know that Doctrine repositories/entity manager exist at all. If need arises, you can change UserRepository implementation to load users from remote API, from file on disk....from anywhere.
Model manipulation
It allows you to manipulate your models before they get to business logic, allowing you to calculate values not persisted as a field in database. Eg, to fetch them from Redis, from some API or other...
Cleaner API
It makes it really obvious what abilities your system has, making understanding easier and allowing easier testing.
Performance optimisation
It doesn't suffer from performance issues as first approach. Take the following example:
You have $eventsCount field on your User model.
If you load list of 100 users and use first approach, you would need to fire 100 queries to count number of events belonging to each user.
SELECT COUNT(*) FROM events WHERE user_id = 1;
SELECT COUNT(*) FROM events WHERE user_id = 2;
SELECT COUNT(*) FROM events WHERE user_id = 3;
SELECT COUNT(*) FROM events WHERE user_id = 4;
...
If you have your own UserRepository implementation, however, you can just make method getEventCountsForUsers($userIds) which would fire one query:
SELECT COUNT(*) FORM events WHERE user_id IN (:user_ids) GROUP BY user_id;
You can implement your own repository to include your own sql queries, Symfony have documented it pretty well in their documentation, see here.
Here's how I've done it previously using annotations (this can be done via yaml too, just check the link above)...
Entity:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
*/
class User
{
// ...
private $statistic;
// ...
}
User Repository:
namespace AppBundle\Entity;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function getUserStats()
{
// Use query builder to build your query for stats here
}
}
Since your custom repository is extending the EntityRepository, you will still have access to Doctrines lazy load methods (find, findBy, findAll etc...)
Maybe you don't need exactly User instance.
You can use NEW() syntax in DQL.
class UserStatDTO
{
private $user;
private $statistic;
private $sum;
public function __construct(User $user, $statistic, $sum)
{
$this->user = $user;
$this->statistic = $statistic;
$this->sum = $sum;
}
public function getUser()
{
return $this->user;
}
public function getSum()
{
return $this->sum;
}
public function getStatistic()
{
return $this->statistic;
}
}
class UserRepository
{
public function getUsersWithCalculatedStat()
{
return $this->getEntityManager()->createQuery('
SELECT NEW UserStatDTO(
u, u.statistic, u.count1 + u.count2
) FROM User
')->getResult();
}
}
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/dql-doctrine-query-language.html#new-operator-syntax
Related
I am working on a Symfony 3.4 + Doctrine based project. The production database has grown quite large and I would like to be able to copy some of the data / entities to a second database which can be used as sandbox for running tests, evaluations, etc. on the data.
Adding a second database connection to the project was no problem. Second step was query the database schema / table structure form Doctrine to re-create the exact same tables for some entities in this second DB.
Now I would like to query entities from the production DB using the entity manager as usual and persist them to the second DB. Since the second DB holds only some of the data/entities/tables I cannot use a second entity manager here but have to insert the data manually.
I am looking for something like:
// Load entity from production DB via entity manager
$repo = $this->em->getRepository(SomeEntity::class);
$entity = $repo->findOneById('xy12');
// Pseudocode(!): Get SQL code to save the entity
$saveQuery = $repo->getSaveQueryForEntity(entity); <<< HOW TO DO THIS?
$saveSql = saveQuery->getSql();
// Run SQL on sandbox connection
$sandboxConnection = $doctrine->getConnection('sandbox');
$sandboxConnection->executeQuery($saveSql);
Of course I could create the INSERT query completely manually. However, this would be quite cumbersome and error prone. On the other hand creating the SQL code already build into Doctrine and all I need is a way to access/get this code to run it on a different connection?
Or is this approach completely wrong and there is a better way to get an entity from one DB to the other?
EDIT:
Dumping the complete database and importing it into a sandbox DB is not an option. The database holds data of many registered users and each user can decide if and when he wants to transfer some data to the sandbox. Copying a several GB large database with all user data to a sandbox because User 123 wants to run some tests on entities A and B is not very effective, is it?
I do not want to describe the complete internal logic of the project here, since this does not really help the question. So the question is how to copy / move a single entity to another database by getting the SQL from doctrine :-)
You said:
Since the second DB holds only some of the data/entities/tables I cannot use a second entity manager here but have to insert the data manually.
but you can still declare two different entity managers, that both map the same entity, but with different mapping options (maybe you don't want to map all fields with the other entity manager, for example).
You will need to have two distinct mappings that are bound to the same entity, so better go with separate YML files (instead of annotations). You can have something like:
config/orm/default/User.orm.yml // User mapping for the default EM
config/orm/other/User.orm.yml // User mapping for the other EM
Also, loading the entity with the default entity manager and persisting with the other will not work as expected. You will have to use merge() instead of persist(), since the entity will be managed by the default entity manager:
$user = $defaultEntityManager->getRepository(User::class)->find(1);
$otherEntityManager->merge($user);
$otherEntityManager->flush();
Given it's absolutely unsuitable to do it on a database level using mysqldump, I would probably enable two entity managers for the project and maintain exactly the same data schema for both of them. It gives an opportunity to persist similar objects when needed. When your user would select entities to copy on a web page, we can pass those ids to a handler to fetch entity from the main entity manager and sync it into a sandbox one. It should be pretty straightforward and less hacky than getting insert SQL queries from Doctrine. Here is a simple example to give you a starting point.
<?php
declare(strict_types=1);
namespace App\Sync;
use Doctrine\ORM\EntityManagerInterface;
class SandboxDbSyncHandler
{
/** #var EntityManagerInterface */
private EntityManagerInterface $em;
/** #var EntityManagerInterface */
private EntityManagerInterface $sandboxEm;
public function __construct(EntityManagerInterface $em, EntityManagerInterface $sandboxEm)
{
$this->em = $em;
$this->sandboxEm = $sandboxEm;
}
public function sync(string $class, array $ids): void
{
$repo = $this->em->getRepository($class);
$sandBoxRepo = $this->sandboxEm->getRepository($class);
$entities = $repo->findBy(['id' => $ids]);
foreach ($entities as $entity) {
if (!$entity instanceof UpdatableFromEntity) {
continue;
}
$sandBoxEntity = $sandBoxRepo->find($entity->getId());
if (!$sandBoxEntity) {
$sandBoxEntity = $class()::createFromEntity();
}
$sandBoxEntity->updateFromEntity($entity);
$this->sandboxEm->persist($entity);
}
$this->sandboxEm->flush();
}
}
<?php
declare(strict_types=1);
namespace App\Entity;
interface UpdatableFromEntity
{
public static function createFromEntity($entity);
public function updateFromEntity($entity): void;
}
<?php
declare(strict_types=1);
namespace App\Entity;
class SomeEntity implements UpdatableFromEntity
{
private string $id;
private ?string $name;
public function __construct(string $id)
{
$this->id = $id;
}
public function getId(): string
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function updateFromEntity($entity): void
{
if (!$entity instanceof SomeEntity::class) {
throw new \Exception('Cannot update an entity from the entity of a different type');
}
$this->name = $entity->getName();
}
public static function createFromEntity($entity)
{
if (!$entity instanceof SomeEntity::class) {
throw new \Exception('Cannot create an entity from the entity of a different type');
}
return new static($entity->getId());
}
}
I'm trying to create an application where I have two related models: "User" and "Debtor".
User is a doctrine entity that is persisted in my database.
Debtor is a model that is stored on an external system and accessed through an API. I have already created a repository for accessing Debtors with find(), findAll() and findByUserId().
If "Debtor" was a Doctrine Entity, i would have a #OneToMany relationship on the User model in order to access debtors. Something like this:
/**
* #Entity
*/
class User {
/**
* #OneToMany
*/
private $debtors;
public function getDebtors() {
return $this->debtors;
}
public function setDebtors($debtors) {
$this->debtors = $debtors;
return $this;
}
}
One solution would be to call DebtorRepository::findByUserId($this->id) in the getDebtors() method, but I don't think that is the right way to go.
I also thought about have a #PostLoad method that loads the users debtors into the $debtors property.
I'm not much for having that much logic inside my models, but i don't know what other choices I have?
Furthermore, I also need to be able to access various other models through the Debtor model and so i have the issue again.
If resource for your API, I think you should have service which will take care of loading that (as probably you need to check for errors/authentication/...). So I would load it on demand from service.
But if you really want to have it accessible in the entity, then you can use Entity Listener, hook to the postLoad method and inject it there. In this case your entity will still stay thin, because this heavy logic will be in external service.
Is it considered a bad practice to add fields to Symfony entity in controller? For example lets say that I have a simple entity:
/**
* #ORM\Entity
* #ORM\Table(name="user")
*/
class User extends BaseUser
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
}
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
}
And then in UserController.php I want to do the following:
foreach($users as $user){
$user->postsCount = someMethodThatWillCountPosts();
}
So later that postsCount can be displayed in Twig. Is it a bad practice?
Edit:
It's important to count posts on side of mysql database, there will be more than 50.000 elements to count for each user.
Edit2:
Please take a note that this questions is not about some particular problem but rather about good and bad practices in object oriented programming in Symfony.
As #Rooneyl explained that if you have relation between user and post then you can get count easily in your controller, refer this for the same. But if you are looking to constructing and using more complex queries from inside a controller. In order to isolate, reuse and test these queries, it's a good practice to create a custom repository class for your entity.Methods containing your query logic can then be stored in this class.
To do this, add the repository class name to your entity's mapping definition:
// src/AppBundle/Entity/Product.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProductRepository")
*/
class Product
{
//...
}
Doctrine can generate empty repository classes for all the entities in your application via the same command used earlier to generate the missing getter and setter methods:
$ php bin/console doctrine:generate:entities AppBundle
If you opt to create the repository classes yourself, they must extend
Doctrine\ORM\EntityRepository.
More Deatils
Updated Answer
In many cases associations between entities can get pretty large. Even in a simple scenario like a blog. where posts can be commented, you always have to assume that a post draws hundreds of comments. In Doctrine 2.0 if you accessed an association it would always get loaded completely into memory. This can lead to pretty serious performance problems, if your associations contain several hundreds or thousands of entities.
With Doctrine 2.1 a feature called Extra Lazy is introduced for associations. Associations are marked as Lazy by default, which means the whole collection object for an association is populated the first time its accessed. If you mark an association as extra lazy the following methods on collections can be called without triggering a full load of the collection: SOURCE
"rather about good and bad practices in object oriented programming"
If that's the case then you really shouldn't have any business logic in controller, you should move this to services.
So if you need to do something with entities before passing them to twig template you might want to do that in specific service or have a custom repository class that does that (maybe using some other service class) before returning the results.
i.e. then your controller's action could look more like that:
public function someAction()
{
//using custom repository
$users = $this->usersRepo->getWithPostCount()
//or using some other service
//$users = $this->usersFormatter->getWithPostCount(x)
return $this->render('SomeBundle:Default:index.html.twig', [
users => $users
]);
}
It's really up to you how you're going to do it, the main point to take here is that best practices rather discourage from having any biz logic in controller. Just imagine you'll need to do the same thing in another controller, or yet some other service. If you don't encapsulate it in it's own service then you'll need to write it every single time.
btw. have a read there:
http://symfony.com/doc/current/best_practices/index.html
Lets see my architect:
Model:
// links table: (ID, LINKNAME)
Class Link extends Link_base
{
}
Controller:
public function index()
{
$this->links = new Doctrine - here I build the query, SELECT, ORDER BY, etc
}
in this example, the model can be remain empty (no serious logic), all I need is a select with an order by. Im not sure I can use Doctrine in controller though - should I remake it like this?
Class Link extends Link_base
{
public function getLinks()
{
return new Doctrine - here I build the query, SELECT, ORDER BY, etc;
}
}
Controller:
public function index()
{
$this->links = Links::getLinks();
}
Im not sure which way seems to be OK. Of course, when selecting needs a more complex, formatting todo-s, it goes to the model or helper - but I feel like I just made a new (unnecessary) layer. This getLinks() used only once. In other words: Doctrine may be only used in model, or can it be used in controllers too?
Your entities (or models if you prefer that name) should not know how they are saved to / retrieved from the database. They should just be simple PHP objects, only containing a number of properties (corresponding to the database columns) and their getters and setters.
(If you are interested, read a bit about the single responsibility principle which states that every class should have one, and only one responsibility. If you make your entities both responsible for storing data and knowing how to save that data in the database, you will have a greater chance of introducing bugs when one of those things changes.)
You can fetch entities from inside your controller:
<?php
namespace Your\Bundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class LinkController extends Controller
{
public function fooAction()
{
$links = $this->getDoctrine()
->getRepository('YourBundle:Link')
->findAll();
// do something with the result, like passing it to a template
}
}
However, you might need a more complex query (that includes sorting and filtering) and you might need to run that query from multiple controllers. In that case, you don't want to duplicate that logic to multiple controllers, you want to keep that logic in one central place.
To do so, create a repository:
<?php
namespace Your\Bundle\Repository;
use Doctrine\ORM\EntityRepository;
class LinkRepository extends EntityRepository
{
public function findAllOrderedByName()
{
return $this->getEntityManager()
->createQuery(
'SELECT l FROM YourBundle:Link l ORDER BY l.name ASC'
)
->getResult();
}
}
And add the repository class to your mapping:
Your\Bundle\Entity\Link:
type: entity
repositoryClass: Your\Bundle\Repository\LinkRepository
(check the Symfony's documentation about custom repositories if you're using XML or annotations instead of Yaml)
Now in your controller, you can simply update your fooAction method so it uses your custom repository method:
public function fooAction()
{
$links = $this->getDoctrine()
->getRepository('YourBundle:Link')
->findAllOrderedByName();
}
For more information, Symfony's documentation includes a great article about Doctrine. If you haven't done so already, I'd definately recommend reading it.
To keep it simple, let's suppose an application which has Accounts and Users. Each account may have any number of users. There's also 3 consumers of UserRepository:
An admin interface which may list all users
Public front-end which may list all users
An account authenticated API which should only list it's own users
Assuming UserRepository is something like this:
class UsersRepository extends DatabaseAbstraction {
private function query() {
return $this->database()->select('users.*');
}
public function getAll() {
return $this->query()->exec();
}
// IMPORTANT:
// Tons of other methods for searching, filtering,
// joining of other tables, ordering and such...
}
Keeping in mind the comment above, and the necessity to abstract user querying conditions, How should I handle querying of users filtering by account_id? I can picture three possible roads:
1. Should I create an AccountUsersRepository?
class AccountUsersRepository extends UserRepository {
public function __construct(Account $account) {
$this->account = $account;
}
private function query() {
return parent::query()
->where('account_id', '=', $this->account->id);
}
}
This has the advantage of reducing the duplication of UsersRepository methods, but doesn't quite fit into anything I've read about DDD so far (I'm rookie by the way)
2. Should I put it as a method on AccountsRepository?
class AccountsRepository extends DatabaseAbstraction {
public function getAccountUsers(Account $account) {
return $this->database()
->select('users.*')
->where('account_id', '=', $account->id)
->exec();
}
}
This requires the duplication of all UserRepository methods and may need another UserQuery layer, that implements those querying logic on chainable way.
3. Should I query UserRepository from within my account entity?
class Account extends Entity {
public function getUsers() {
return UserRepository::findByAccountId($this->id);
}
}
This feels more like an aggregate root for me, but introduces dependency of UserRepository on Account entity, which may violate a few principles.
4. Or am I missing the point completely?
Maybe there's an even better solution?
Footnotes: Besides permissions being a Service concern, in my understanding, they shouldn't implement SQL query but leave that to repositories since those may not even be SQL driven.
Fetching all users belonging to an account is more of an UI concern. My suggestion is use your MVC controller(like AccountAdminController?) invoke the UserRepository.findByAccountId() directly.
I think Aggregates should be returned only by its own repository.