Designer:
tableName: designers
actAs:
I18n:
fields: [name]
columns:
id:
type: integer(2)
unsigned: true
primary: true
autoincrement: true
name:
type: string(30)
notnull: true
While default I18n behavior must be used like this
$d = Doctrine_Query::create()
->select('id, t.name')
->from('Designer d')
->leftJoin('d.Translation t')
->where("t.lang = 'en'")
->execute();
I would be faaar more convenient to set some constant to the current language, say en, and have every i18nable field correspond to it, thus having such query
$d = Doctrine_Query::create()
->select('id, name')
->from('Designer d')
->execute();
equivalent to the above one.
I'm trying to make a new behavior, extending the default one, which could provide such things, but I need your help.
Getting the needed language is easy, so let's just say there is define('LANGUAGE', 'en'). The basic behavior class is
class TransparentI18N extends Doctrine_Template
{
private $_translation = NULL;
public function setUp()
{
$this->addListener(new TransparentI18NListener());
$this->actAs(new Doctrine_Template_I18n($this->_options));
}
}
So the idea is to add a listener, that would modify the query to set joins and select needed fields whenever such fields are present in the select clause. The TransparentI18NListener contains the preDqlSelect, which receives the Doctrine_Event object. And I can get the relevant Doctrine_Query and even getDqlPart('select') works, but the latter returns the raw select string, like id, t.name, how can I get, to which table each clause corresponds?
Then I'd have to set fields of the Doctrine_Record instance. Is it possible without making models extend some custom class which would give such functionality? I'm really reluctant to add such a class, but in case everything other fails, I should be able to make it save these fields and override the __get method to show translated fields if they are requested.
I was too scared to think about the insert/update/delete part, but hopefully if I deal with the above problems, I'd be able to add hooks to dql and get the job done.
Do you think such a think is possible in general without messing with core doctrine libraries? Having to use it in the default way would be a huge pain in the *...
I don't have a good, non-hacky solution for your actual problem but have an alternative to try.
You could put all your find/get queries in the models corresponding table class.
So you still have to do the Translation-Joints but all you queries are at one place and are easy to maintain.
class DesignerTable extends Doctrine_Table
{
protected $_lang = LANGUAGE;
public function findAll()
{
return Doctrine_Query::create()
->select('id, t.name')
->from('Designer d')
->leftJoin('d.Translation t')
->where("t.lang = ?", $this->_lang)
->execute();
}
public function findOneById($id)
{
return Doctrine_Query::create()
->select('id, t.name')
->from('Designer d')
->leftJoin('d.Translation t')
->where('.lang = ?', $this->_lang)
->andWhere('id = ?', $id)
->execute();
}
...
}
// In your controllers:
// Find all designers
$designers = Doctrine_Core::getTable('Designer')->findAll();
// Find one designer by id
$designers = Doctrine_Core::getTable('Designer')->findOneById(13);
Related
Long story short.
I use Doctrine's Single Table Inheritance mapping to map three different contexts (classes) of the one common entity: NotActivatedCustomer, DeletedCustomer, and Customer. Also, there is an AbstractCustomer which contains the next:
App\Identity\Domain\Customer\AbstractCustomer:
type: entity
inheritanceType: SINGLE_TABLE
discriminatorColumn:
name: discr
type: string
discriminatorMap:
Customer: App\Identity\Domain\Customer\Customer
NotActivatedCustomer: App\Identity\Domain\Customer\NotActivatedCustomer
DeletedCustomer: App\Identity\Domain\Customer\DeletedCustomer
table: customer
id:
id:
type: customer_id
unique: true
generator:
strategy: CUSTOM
customIdGenerator:
class: Symfony\Bridge\Doctrine\IdGenerator\UuidV4Generator
fields:
email:
type: email
length: 180
unique: true
A Subtype definition example:
<?php
declare(strict_types=1);
namespace App\Identity\Domain\Customer;
use App\Identity\Domain\User\Email;
class DeletedCustomer extends AbstractCustomer
{
public const TYPE = 'DeletedCustomer';
public function __construct(CustomerId $id)
{
$this->_setId($id);
$this->_setEmail(new Email(sprintf('%s#mail.local', $id->value())));
}
}
The Use Case:
<?php
declare(strict_types=1);
namespace App\Identity\Application\Customer\UseCase\DeleteCustomer;
use App\Identity\Application\Customer\CustomerEntityManager;
use App\Identity\Application\User\AuthenticatedCustomer;
use App\Identity\Domain\Customer\DeletedCustomer;
use App\Shared\Application\ImageManager;
final class DeleteCustomerHandler
{
private CustomerEntityManager $customerEntityManager;
private AuthenticatedCustomer $authenticatedCustomer;
private ImageManager $imageManager;
public function __construct(AuthenticatedCustomer $authenticatedCustomer,
CustomerEntityManager $customerEntityManagerByActiveTenant,
ImageManager $customerPhotoManager)
{
$this->customerEntityManager = $customerEntityManagerByActiveTenant;
$this->authenticatedCustomer = $authenticatedCustomer;
$this->imageManager = $customerPhotoManager;
}
public function handle(): void
{
$customer = $this->authenticatedCustomer->customer();
$photo = (string) $customer->photo();
$deletedCustomer = new DeletedCustomer($customer->id());
// TODO OR return DeletedCustomer that way
// $deletedCustomer = $customer->deactive();
// entityManager->merge() called here
$this->customerEntityManager->sync($deletedCustomer);
// simple entityManager->flush() under the hood
$this->customerEntityManager->update();
// that's a raw query to update discriminator field, hackish way I'm using
// UPDATE customer SET discr = ? WHERE id = ?
$this->customerEntityManager->updateInheritanceType($customer, DeletedCustomer::TYPE);
if ($photo) {
$this->imageManager->remove($photo);
}
}
}
So if you have already an existing Customer persisted and run DeleteCustomerHandler, the Customer will be updated, but its discriminator field won't!
Googling that, there is no way to update the discriminator field not going some hackish way like I do (running raw query manually to update the field).
Also, I need to use the EntityManager->merge() method to add manually initialized DeletedCustomer to internal UnitOfWork. Looks a little bit dirty too, and it's a deprecated method for Doctrine 3, so the question also is there a better way to handle my case?
So, to conclude all the questions:
Am I doing Customer's status change to DeletedCustomer completely wrong? I'm just trying to avoid Customer God Object, distinguish this Entity's bounded contexts, kinda that.
How to avoid EntityManager->merge() there? AuthenticatedCustomer comes from session (JWT).
I think you're absolutely right to want to avoid Customer turning into a god object. Inheritance is one way to do it, but using it for customers in different statuses can lead to problems.
The two key problems in my experience:
As new statuses emerge, will you keep adding different inherited entities?
What happens when you have a customer move through two different statuses, such as a customer that was a NotActivatedCustomer but is now a DeletedCustomer?
So I keep inheritance only when the inherited type is genuinely more specific type, where a given entity will only ever be one of those types for its entire lifecycle. Cars don't become motorbikes, for example.
I have two patterns for solving the problem differently to you. I tend to start with the first and move to the second.
interface DeletedCustomer
{
public function getDeletedAt(): DateTime;
}
interface NotActivatedCustomer
{
public function getCreatedAt(): DateTime;
}
class Customer implements DeletedCustomer, NotActivatedCustomer
{
private $id;
private $name;
private DateTime $deletedAt;
private bool $isActivated = false;
public function getDeletedAt(): DateTime {...}
public function getCreatedAt(): DateTime {...}
}
class DeletedCustomerRepository
{
public function findAll(): array
{
return $this->createQuery(<<<DQL
SELECT customer
FROM Customer
WHERE customer.deletedAt IS NOT NULL
>>>)->getQuery()->getResults();
}
}
class NotActivatedCustomerRepository
{
public function findAll(): array
{
return $this->createQuery(<<<DQL
SELECT customer
FROM Customer
WHERE customer.isActivated = false
>>>)->getQuery()->getResults();
}
}
class DeletedCustomerService
{
public function doTheThing(DeletedCustomer $customer) {}
}
This reduces coupling, which is one of the main problems with god objects. So when the columns start to proliferate, I can move them off to real entities that join to the Customer. Components that refer to DeletedCustomer will still receive one.
The second pattern is event-sourcing-lite - have a many-to-one relationship with a "CustomerLifecycleEvent" entity. Query based on whether the customer has a "deleted" event. This second approach is much more complex, both to update and query. You can still have dedicated repositories that return entities like DeletedCustomer, but you'll need to do a bit more boilerplate.
I have to simple Entity: Log and User.
Log has a ManyToOne relationship with Entity.
Log:
type: entity
repositoryClass: LogRepository
id:
id:
type: integer
generator:
strategy: AUTO
fields:
message:
type: string
manyToOne:
user:
targetEntity: User
joinColumns:
user_id:
referencedColumnName: id
My use case is to show the list of the logs and one or two information about the user (like his name and his mail for example)
If I use the findall method, Symfony debug toolbar shows me that Doctrine performs a lot of queries. One query gives me the logs and one query is performed for each user! It is not good of course because I can have thousand logs in my view. I don't want to overload my database server. This problem seems very simple to solve. But I'm searching for a while and the results seems to be "bad practices".
So I began by writing a new method in the LogRepository class using the querybuilder:
public function getLog(){
$qb = $this->createQueryBuilder('l')
->select('l')
->innerJoin(
'ApplicationSonataUserBundle:User', 'u',
Expr\Join::WITH,'l.user = u.id')
;
return $qb->getQuery()->getResult();
}
I still had the same problem. I have changed the select parameters on my method to :
public function getLog(){
$qb = $this->createQueryBuilder('l')
->select('l','u')
->innerJoin('ApplicationSonataUserBundle:User','u',
Expr\Join::WITH,'l.user = u.id')
;
return $qb->getQuery()->getResult();
}
Eureka? OK, I only have one query but my method didn't return only Log, BUT User too... So my Twig template crashes because my loop contains User, not only Log. When this is a User, my view crash because I want to write message fields. (Log.message exists. But User.message is not a valid field, of course)
It works pretty good, if I change one more time my method with a loop to filter my results :
public function getLog(){
$qb = $this->createQueryBuilder('l')
->select('l','u')
->innerJoin('ApplicationSonataUserBundle:User','u',
Expr\Join::WITH,'l.user = u.id')
;
//THE STRANGE LOOP
$results = array();
foreach ($qb->getQuery()->getResult() as $result){
if ($result instanceof Log){
$results[] = $result;
}
};
return $results;
}
I have only one query, it is what I'm searching for. My twig template doesn't crash, because my array contains only Log.
So what's the matter? It works, but I think this is not the good/best practices.
Someone can explain me a better way, a better practice to use an inner join query, to minimize the performed query and have an ArrayCollection result which contains only instance of Log?
It should not be necessary to use the loop. Try like this:
public function getLog(){
$qb = $this->createQueryBuilder('l')
->select('l','u')
->innerJoin('l.user', 'u');
$logs = $qb->getQuery()->getResult();
return $logs;
}
It should only return $logs with a populated (fetch joined) association user.
I'm working with Entity objects from Doctrine queries and i end up with a very big array, with all information from all entities related. This ends up being a huge data tree... how can i limit this? Avoid listing all data from all relationships?
You can always remove not needed associations (this is a best practice for speeding up Doctrine). Or you can select only fields that you need in your presentation layer (as read-only data):
public function getAll()
{
$qb = $this->createQueryBuilder('u'); // Where are in User custom repository
return $qb
->select(array('u.id', 'u.first', 'u.last'))
->getQuery()
->getResult();
}
If you still need to work with objects (or for complex queries that needs plain SQL) a possibility is filling only needed properties (and eventually, associations/nested collections) of your domain object.
An example, more on native SQL:
public function getAll()
{
$mapping = new \Doctrine\ORM\Query\ResultSetMapping();
$mapping->addEntityResult('Acme\HelloBundle\User', 'e');
$mapping->addFieldResult('e', 'id', 'id');
$mapping->addFieldResult('e', 'first', 'first');
$mapping->addFieldResult('e', 'last', 'last');
$sql = "SELECT id, first, last FROM user ";
$result = $this->_em->createNativeQuery($sql, $mapping)->getResult();
// Or hust return $result itself (array)
return new \Doctrine\Common\Collections\ArrayCollection($result);
}
Of course the disadvance (?) is use of native SQL. I don't believe that ResultSetMapping can be used with DQL.
EDIT: take a look at http://docs.doctrine-project.org/projects/doctrine-orm/en/2.0.x/reference/best-practices.html
In my Symfony/Doctrine application, I would like any timestamp fields in my database to retreived as PEAR Date objects instead of date strings. e.g. If my schema is
SomeEvent:
columns:
id:
type: integer
primary: true
start: timestamp
end: timestamp
I would like to be able to run a Doctrine query to retrieve SomeEvent objects and have $anEvent->getStart() be a PEAR Date object. Right now Doctrine gives me strings for all timestamp fields which is basically useless. (I'd also like saving of Dates to work correctly.)
What's the best way to accomplish this?
I researched using a Hydration listener but it looks like I'd have to register that per table and hardcode the column names that I want to be converted. Using a custom Hydrator didn't look much better since then I lose the ability to use any of the other core hydration methods without having my dates be strings again.
EDIT: It looks like Doctrine 2 has a feature that's exactly what I'm looking for: Custom Mapping Types. Unfortunately this site is being deployed to a host that doesn't support PHP 5.3+ so Doctrine 2 is out. :(
I figured out a hacky way to do it that I'm not super happy with, but it works. I created the following 2 classes:
class DateClassBehavior extends Doctrine_Template {
public function setTableDefinition() {
$listener = new DateClassListener();
$table = $this->getTable();
foreach ($table->getColumns() as $columnName => $columnDef) {
if ($columnDef['type'] == 'timestamp') {
$listener->dateColumns[] = $columnName;
}
}
$this->addListener($listener);
}
}
class DateClassListener extends Doctrine_Record_Listener {
public $dateColumns = array();
public function postHydrate(Doctrine_Event $event) {
$data = $event->data;
foreach ($this->dateColumns as $col) {
$date = $data->$col == null ? null : new Date($data->$col);
$data->$col = null;
$data->$col = $date;
}
$event->data = $data;
}
}
And added DateClassBehavior to the definition for each table in my model:
SomeEvent:
actAs: [DateClassBehavior]
columns:
id:
type: integer
primary: true
start: timestamp
end: timestamp
This takes care of creating the Date objects when things are loaded. I also modified the actual PEAR Date class (I know... bad) and added a __toString() method that returns $this->getDate(). PHP then automatically converts the dates to the correct string when doctrine saves them.
If anyone finds a better way to do this please post it.
I have models that extend Doctrine_Record, and are directly mapped to one specific record from the database (that is, one record with given a given id, hardcoded statically in the class).
Now, I want the specific record class to initialize itself as if Doctrine_Query would. So, this would be the normal procedure:
$query = new Doctrine_Query();
$model = $query->from('Model o')->where('id = ?', 123)->fetchOne();
I would like to do something like this
$model = new Model();
And in the Model:
const ID = 123;
//note that __construct() is used by Doctrine_Record so we need construct() without the __
public function construct()
{
$this->id = self::ID;
//what here??
$this->initialize('?????');
}
So for clarity's sake: I would like the object to be exactly the same as if it would be received from a query (same state, same attributes and relations and such).
Any help would be greatly appreciated: thank you.
The first thing I need to say is I'd put the constant in the class. So like this:
class Application_Model_Person
{
const ID = 1234;
}
Then, a Doctrine method like Doctrine_Record::fetchOne() is always returning a (new) instance of the model and never merges the data with the record you're calling fetchOne() to. Doctrine is nevertheless able to merge a retreived record with another class, so it rather simple to do:
class Application_Model_Person extends Doctrine_Record_Abstract
{
const ID = 1234;
public function __construct($table = null, $isNewEntry = false)
{
// Calling Doctrine_Record::__construct
parent::__construct($table, $isNewEntry);
// Fetch the record from database with the id self::ID
$record = $this->getTable()->fetchOne(self::ID);
$this->merge($record);
}
}
Then you're able to do:
$model = new Application_Model_Person;
echo $model->id; // 1234
Although having multiple classes for the same data type (i.e. table) is really not what ORM should be like, what you want can be done in Doctrine using Column aggregation inheritance. Assuming you are using Doctrine 1.2.x, you can write the following YML:
Vehicle:
columns:
brand: string(100)
fuelType: string(100)
Car:
inheritance:
extends: Entity
type: column_aggregation
keyField: type
keyValue: 1
Bicycle:
inheritance:
extends: Entity
type: column_aggregation
keyField: type
keyValue: 2
Now, the Vehicle table will have a 'type' column, that determines the class that Doctrine will instantiate when you select a vehicle. You will have three classes: Vehicle, Car and Bicycle. You can give every class their own methods etc, while the records their instances represent reside in the same database table. If you use $a = new Bicycle, Doctrine automatically sets the type for you so you don't have to take care of that.
I don't think a model instance could decide to hang on a certain database entry after it has been initialized. That said, you can do something like this:
<?php
class Model extends baseModel {
public static function create($id = null)
{
if ($id === null) return new Model;
return Doctrine::getTable('Model')->findeOneById($id);
}
}
And then, you can either use
$newModel = Model::create();
Or fetch an existing one having ID 14 (for example) using
$newModel = Model::create(14);
Or, if you want your 123 to be default instead of a new item, declare the function like this:
public static function create($id = 123)