I'm looking at using doctrine for an application I'm working on - but after reading the documentation I'm having trouble conceptualizing how to represent the database structure we have in terms of entities.
I have many tables which have partner tables which hold translation data like the following....
Where I would like to have one Entity (Navigation Element) which had access to the 'label' field depending on what Language I set in my application. The following from the Doctrine documentation seems to suggest that you need to define one (single) table which is used to persist an entity
http://www.doctrine-project.org/docs/orm/2.0/en/reference/basic-mapping.html
By default, the entity will be
persisted to a table with the same
name as the class name. In order to
change that, you can use the #Table
annotation as follows:
Or do I need to define two entities and link them (or allow the translation table to inherit from the element table).
And what strategy would I use to always insert a language_id clause to the Join (to ensure I'm pulling the right label for the currently set language). Is this something I would define in the entity itself, or elsewhere?
This seems to suit a One-To-Many Bidirectional association. This is the scenario from that page translated to your situation:
/** #Entity */
class NavigationElement
{
// ...
/**
* #OneToMany(targetEntity="NavigationElementTranslation", mappedBy="navigationElement")
*/
private $translations;
// ...
public function __construct() {
$this->translations = new \Doctrine\Common\Collections\ArrayCollection();
}
}
/** #Entity */
class NavigationElementTranslation
{
// ...
/**
* #ManyToOne(targetEntity="NavigationElement", inversedBy="translations")
* #JoinColumn(name="navigation_element_id", referencedColumnName="id")
*/
private $navigationElement;
// ...
}
You could add a getLabel($languageId) method to the NavigationElement entity that searches through the translations to get the correct label:
public function getLabel($languageId) {
foreach($this->translations as $trans) {
if($trans->languageId == $languageId)
return $trans->label;
}
throw new InvalidArgumentException();
}
And you could use the following DQL to ensure you only load the translation you want into the $translations property:
$query = $em->createQuery(
"SELECT ne, net
FROM Entity\NavigationElement ne
JOIN ne.translations net WITH net.languageId = :langId"
);
$query->setParameter('langId', $languageId);
$navigationElements = $query->execute();
This situation sounds like one where you would want to cache aggressively. Make sure you look into Doctrine 2's caching mechanisms too.
Also, internationalization can be handled reasonably well in PHP with gettext if you find join tables for translations start to become unmanageable.
I would also direct anyone who has to tackle this same problem to take a look at the following doctrine extension.
http://www.gediminasm.org/article/translatable-behavior-extension-for-doctrine-2
Related
I am using TYPO3 8. In my extension I have a database table "company" in which I store for each company the total number of places (number_places) and the number of occupied places (occupied_places).
Now I want to limit the search to companies which have available places left.
In MySQL it would be like this:
SELECT * FROM company WHERE number_places > occupied_places;
How can I create this query in the extbase repository?
I tried to introduce the virtual property placesLeft in my model but it did not work.
I don't want to use a raw SQL statement as mentioned below, because I already have implemented a filter which uses plenty of different constraints.
Extbase query to compare two fields in same table
You can do it like this in your repository class, please note the comments inside the code:
class CompanyRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
public function findWithAvailablePlaces(bool $returnRawQueryResult = false)
{
// Create a QueryBuilder instance
$queryBuilder = $this->objectManager->get(\TYPO3\CMS\Core\Database\ConnectionPool::class)
->getConnectionForTable('company')->createQueryBuilder();
// Create the query
$queryBuilder
->select('*')
->from('company')
->where(
// Note: this string concatenation is needed, because TYPO3's
// QueryBuilder always escapes the value in the ExpressionBuilder's
// methods (eq(), lt(), gt(), ...) and thus render it impossible to
// compare against an identifier.
$queryBuilder->quoteIdentifier('number_places')
. \TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder::GT
. $queryBuilder->quoteIdentifier('occupied_places')
);
// Execute the query
$result = $queryBuilder->execute()->fetchAll();
// Note: this switch is not needed in fact. I just put it here, if you
// like to get the Company model objects instead of an array.
if ($returnRawQueryResult) {
$dataMapper = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper::class);
return $dataMapper->map($this->objectType, $result);
}
return $result;
}
}
Notes:
If you have lots of records to deal with, I would - for performance reasons - not use the data mapping feature and work with arrays.
If you want to use the fluid pagination widget, be sure you don't and build your own pagination. Because of the way this works (extbase-internally), you'd get a huge system load overhead when the table grows. Better add the support for limited db queries to the repository method, for example:
class CompanyRepository extends \TYPO3\CMS\Extbase\Persistence\Repository
{
public function findWithAvailablePlaces(
int $limit = 10,
int $offset = 0,
bool $returnRawQueryResult = false
) {
// ...
$queryBuilder
->setMaxResults($limit)
->setFirstResult($offset);
$result = $queryBuilder->execute()->fetchAll();
// ...
}
}
I think you cant do this using the default Extbase Query methods like equals() and so on. You may use the function $query->statement() for your specific queries like this.
You also can use the QueryBuilder since TYPO3 8 which has functions to compare fields to each other:
https://docs.typo3.org/typo3cms/CoreApiReference/latest/ApiOverview/Database/QueryBuilder/Index.html#quoteidentifier-and-quoteidentifiers
It's fine to use this QueryBuilder inside Extbase repositories. After this you can use the DataMapper to map the query results to Extbase models.
In case of using "statement()" be aware of escaping every value which may cause any kind of SQL injections.
Based on the current architecture of TYPO3, the data structure is such that comparing of two tables or, mixing results from two tables ought to be done from within the controller, by injecting the two repositories. Optionally, you can construct a Domain Service that can work on the data from the two repositories from within the action itself, in the case of a routine. The service will also have to be injected.
Note:
If you have a foreign relation defined in your table configuration, the results of that foreign relation will show in your defined table repository. So, there's that too.
Usage of 't' in model relations can give the error
"Column not found: 1054 Unknown column 't.etc' in 'on clause'."
It's the usage of 't' to refer to the current table in CActiveRecord Model relations.
I often stumble accross it when using ,findAll, CActiveDataProvider, etc
Sometimes it works sometimes it doesn't depending on what model you execute and from where.
I tried using tableAlias but it doesn't work. There must be an easy way.
How can I setup my models and it's relations in such a way that the relations are stable
and always works?
Here is an example of two classes to show the problem...
class Order extends CActiveRecord
{
/**
* #return array relational rules.
*/
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'author'=>array(self::BELONGS_TO, 'User', 'user_id'),
'shopproduct'=>array(self::BELONGS_TO, 'ShopProduct', 'product_id',
'with'=>array(
'tagsrelations',
),
),
);
}
}
class ShopProduct extends CActiveRecord
{
/**
* #return array relational rules.
*/
public function relations()
{
// NOTE: you may need to adjust the relation name and the related
// class name for the relations automatically generated below.
return array(
'author'=>array(self::BELONGS_TO, 'User', 'user_id'),
'tagsrelations'=>array(self::HAS_MANY, 'TagsRelations','',
'on'=>' tagsrelations.tbl_uuid = t.uuid ',
'with'=>'tag',
'order'=>'tag.name ASC',
),
);
}
}
// works
$model=Order::model()->with('shopproduct')->findAll();
// doesn't work
// "Column not found: 1054 Unknown column 't.uuid' in 'on clause'. The SQL statement executed was"
$model=ShopProduct::model()->with('tagsrelations')->findAll();
Maybe someone can explain why this isn't working in an understanding way.
How to fix this one once and for all. BELONG TO relations usually work.
What is the best to make an HAS MANY relationship that always works
Do you need the t only for the join or for some specific purpose? Because for the join this is not the right way to do it. It should be done sth like this:
'tagsrelations'=>array(self::HAS_MANY, 'TagsRelations', array('tbl_uuid'=>'uuid')),
Of course you can then add the with condition if you need that as well. But the join should be done this way.
I am new with doctrine 2.
Why Doctrine 2 not have basic validate method that validate if all values fit entities attributes?
My question target to understand more how doctrine 2 works and why without say that something wrong in doctine 2. (Mostly because i am new i miss some understanding about doctrine 2 way of design)
Example:
<?php
// entities/User.php
/**
* #Entity #Table(name="users")
**/
class User
{
/**
* #Id #GeneratedValue #Column(type="integer")
* #var int
**/
protected $id;
/**
* #Column(type="string")
* #var string
**/
protected $name;
}
code example of use of build in validate(not need connect to db, only validate #Column(type="integer") ) basic function that not exist in doctrine 2:
$user=new User();
$user->setId('trtr');
$user->setName("goodname");
if($user->validate()){
echo 'ok';
}
else{
echo $user->validateError();
}
//output: id of User should be integer and not string
Thanks
Doctrine ORM assumes that entities you're persisting are in a valid state. That's the only job of the persistence layer, and adding validation to it would just be wrong. If you have entities with invalid data in them, you already have an invalid object graph that should not be saved.
So please keep in mind that if you ever had some API like
$someEntity->isValid();
Then something is probably wrong, since the entity should always be valid, and any dependencies of it should be set at construction time and handled in setters/getters so that the object never reaches an inconsistent state.
The main reason is separation of concerns. Since entities are fairly dumb objects that don't know much about the rest of the world, their ability to do validations is limited to begin with.
For instance, there's no way that your typical entity could validate that a particular property is unique.
That said, if you just want to do basic validations, just do them in the setters.
<?php
class MyEntity {
// ...
/**
* #ORM\Column(length="32")
*/
protected $myProperty;
public function setMyProperty($prop){
if (! is_string($prop))
throw new \InvalidArgumentException('MyEntity::setMyProperty() expects a string!';
if (strlen($prop) > 32)
throw new \LengthException('Argument passed to MyEntity::setMyProperty() is too long!');
$this->myProperty = $prop;
}
}
This approach can be used to enforce data types, lengths, etc. Anything beyond that is better handled somewhere other than inside your entity.
It's not good idea to mix entity and validation, but it make sense to have this rules in entity as annotation and validation logic in separated aspect validator class.
Check how it's done in Spring framework -
http://www.abtosoftware.com/blog/form-validation-using-aspect-oriented-programming-aop-in-spring-framework
and how to implement it with doctrine2 and go -
http://go.aopphp.com/blog/2013/07/21/implementing-logging-aspect-with-doctrine-annotations/
I have the following entities
Product Entity
Category Entity
Tag Entity
Brand Entity
What I am trying to do is when I get the Product by id, I want to convert it to array including all associated entities including their associated entries.
The result array needs to be serializable.
Use doctrine query builder and the HYDRATE_ARRAY hydration mode?
Edit: Sorry for not including examples, I was on my mobile at the time. Check out the blog post I wrote on some good practices with doctrine that semi-relates to this.
For a code example the repository method I would write to cover this would be as follows (I would avoid using abbreviations like p c etc. as it makes your code a lot less readable (at least while you are getting started with doctrine...)
<?php
namespace Vendor\Prefix\Repository;
use Doctrine\ORM\EntityRepository;
class ProductRepository extends EntityRepository
{
public function find($id)
{
// $em is the entity manager
$qb = $em->createQueryBuilder();
$qb
->select('product', 'category', 'tag', 'brand', 'category_child')
// Or SomeBundle:Product if you're on symfony
->from('Vendor\Prefix\Entity\Product', 'product')
// You need to explicitly fetch join all your
// associations...and select them
->leftJoin('product.Brand', 'brand')
->leftJoin('product.Tags', 'tag')
->leftJoin('product.Categories', 'category')
->leftJoin('category.Children', 'category_child')
// Use prepared statments...its a good habit
->where($qb->expr()->eq('product.ID', ':id'))
->setParameter('id', $id)
;
$query = $qb->getQuery();
// Potential Hydration Modes
// --------------------------------
// Doctrine\ORM\Query::HYDRATE_OBJECT
// Doctrine\ORM\Query::HYDRATE_ARRAY
// Doctrine\ORM\Query::HYDRATE_SCALAR
// Doctrine\ORM\Query::HYDRATE_SINGLE_SCALAR
// Doctrine\ORM\Query::HYDRATE_SIMPLEOBJECT
// Hydrate the result as an array to get the requested format
// When you use array hyrdation doctrine does it according
// to your entity graph
return $query->getResult(Doctrine\ORM\Query::HYDRATE_ARRAY);
}
}
I think you need to use JOIN in DQL syntax:
$results = $entityManager->executeQuery('SELECT p FROM Product p JOIN p.Brand b JOIN p.Tags t JOIN p.Categories c')->getArrayResult();
var_dump($results);
Also, I think you should avoid class members starting with an uppercase letter. It could introduce a bit of confusion with the entity name.
In my application, i have fields that are common to all tables, like create date, update date etc. To assign these values i'm using beforeValidate callback. Now, this callback is same for all models.
To avoid code duplication, i want to create a base model class.
But, when I tried to create a base model, yii thrown error saying table cannot be found in database, which is true since I dont have any table for this base model.
Is there any way I can create a base model class.
Yes, if you work with dynamic DB structure or have other reasons to work with Yii ActiveRecord without creating classes for each table in DB, you may use smartActiveRecord yii extension
I separated it few minuts ago from my other extension -- AR behavior that adds versioning to any model (it copies all data on insert & update to special table (and create it if it's absent), that have a same structure as source table + "revision field" and primary key extended by this field.
Look at SmartAR.php source, there is example of usage in comments.
Take a look at CTimeStampBehavior.
Incase that doesn't help you, you can just write a behavior class yourself.
Hope this helps.
Edit:Assuming you are using ActiveRecords.
If you want to create a new base model, you can do this:
abstract class MyBaseARClass extends CActiveRecord{
protected function beforeValidate(){
if(parent::beforeValidate()){
// assign your fields
return true;
}
else return false;
}
}
Have you created a base table? Thinking about the Yii framework it may be easier to have a relationship between a model and the base model.
In your case, you need to override
public static function model($className=__CLASS__)
{
return parent::model($className);
}
in every child class so Yii would know which table to use for your model. Otherwise it will try and use base class as table name.
I.e.
class User extends BaseActiveRecord {
public static function model($className=__CLASS__)
{
return parent::model($className);
}
}