I need some help when select only discriminator column from doctrine 2 when run the DQL below
SELECT p.type FROM AppBundle\Entity\Product p
type is discriminator column in entity AppBundle\Entity\Product
#ORM\DiscriminatorColumn(name="type", type="smallint")`
#ORM\DiscriminatorMap({
"0" = "AppBundle\Entity\Product",
"1" = "AppBundle\Entity\Product\SingleIssue",
"2" = "AppBundle\Entity\Product\CountBasedIssue",
"3" = "AppBundle\Entity\Product\TimeBasedIssue"
})
I know that type is not a real property in entity, but is there anyway for me to do that?
Thanks in advance!
Updated
After 2 days for reading Doctrine codes, I decided to override SqlWalker and create new Hydrator by the snippets below
Override SqlWalker
<?php
namespace ...;
use Doctrine\ORM\Query\SqlWalker;
class CustomSqlWalker extends SqlWalker
{
const FORCE_GET_DISCRIMINATOR_COLUMN = 'forceGetDiscriminatorColumn';
const DISCRIMINATOR_CLASS_MAP = 'discriminatorClassMap';
/**
* {#inheritdoc}
*/
public function walkSelectClause($selectClause)
{
$sql = parent::walkSelectClause($selectClause);
$forceGetDiscriminatorColumn = $this->getQuery()->getHint(self::FORCE_GET_DISCRIMINATOR_COLUMN);
if (empty($forceGetDiscriminatorColumn)) {
return $sql;
}
foreach ($this->getQueryComponents() as $key => $queryComponent) {
if (!in_array($key, $forceGetDiscriminatorColumn)) {
continue;
}
$metadata = $queryComponent['metadata'];
$discriminatorColumn = $metadata->discriminatorColumn['name'];
$tableName = $metadata->table['name'];
$tableAlias = $this->getSQLTableAlias($tableName, $key);
$discriminatorColumnAlias = $this->getSQLColumnAlias($discriminatorColumn);
$sql .= ", $tableAlias.$discriminatorColumn AS $discriminatorColumnAlias";
}
return $sql;
}
}
Custom Hydrator
<?php
namespace ...;
use Doctrine\ORM\Internal\Hydration\ArrayHydrator;
use PDO;
class CustomHydrator extends ArrayHydrator
{
/**
* {#inheritdoc}
*/
protected function hydrateAllData()
{
$result = array();
$rootClassName = null;
if (isset($this->_hints['forceGetDiscriminatorColumn']) &&
isset($this->_hints['discriminatorClassMap'])) {
$rootClassName = $this->_hints['discriminatorClassMap'];
}
while ($data = $this->_stmt->fetch(PDO::FETCH_ASSOC)) {
foreach ($data as $key => $value) {
if ($this->hydrateColumnInfo($key) != null ||
empty($rootClassName)) {
continue;
}
$metadata = $this->getClassMetadata($rootClassName);
$discriminatorColumn = $metadata->discriminatorColumn;
$fieldName = $discriminatorColumn['fieldName'];
$type = $discriminatorColumn['type'];
$this->_rsm->addScalarResult(
$key, $fieldName, $type
);
}
$this->hydrateRowData($data, $result);
}
return $result;
}
}
Configure custom hydrator
orm:
...
hydrators:
CustomHydrator: YourNamespace\To\CustomHydrator
Final step
$query = $queryBuilder->getQuery();
$query->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, 'YourNamespace\To\CustomSqlWalker');
$query->setHint(\YourNamespace\To\CustomSqlWalker::FORCE_GET_DISCRIMINATOR_COLUMN, array($rootAlias)); // this alias will be used in CustomSqlWalker class
$query->setHint(\YourNamespace\To\CustomSqlWalker::DISCRIMINATOR_CLASS_MAP, $this->getClassName()); // this full-qualify class name will be used in CustomHydrator class
$products = $query->getResult('CustomHydrator');
TL;DR
I know this is a very complicated solution (may be just for my scenario), so I hope someone could give me another simple way to fix that, thanks so much!
There is no direct access to the discriminator column.
It may happen that the entities of a special type should be queried.
Because there is no direct access to the discriminator column,
Doctrine provides the INSTANCE OF construct.
You can query for the type of your entity using the INSTANCE OF DQL as described in the docs. As example:
$query = $em->createQuery("SELECT product FROM AppBundle\Entity\AbstractProduct product WHERE product INSTANCE OF AppBundle\Entity\Product");
$products = $query->getResult();
Hope this helps
I use this little "hack"
Define a common interface for your entities (optional but recommended)
Create a getType method in this interface
Create constant into Discriminator entity
Return the proper constant inside every discriminated entity
That way you can retrieve the discriminator "generic" entity (Product in your case) and call getType onto it.
Of course if you're interested into result filtering done directly by sql, this is not a solution at all and, I'm afraid, there isn't any solution available at the moment.
If you find one better that this, please share with us.
You should be able to do this with a scalar result with INSTANCE OF and a case, when, (else,) end clause:
SELECT
(case
when p INSTANCE OF AppBundle\Entity\Product then \'0\'
when p INSTANCE OF AppBundle\Entity\Product\SingleIssue then \'1\'
when p INSTANCE OF AppBundle\Entity\Product\CountBasedIssue then \'2\'
when p INSTANCE OF AppBundle\Entity\Product\TimeBasedIssue then \'3\'
else \'foobar\'
end) as type
FROM
AppBundle\Entity\Product p
Of course the disadvantage is you have to update the query every time you add a DiscriminatorMap entry.
Related
Assume I have Product entities and Review entities attached to products. Is it possible to attach a fields to a Product entity based on some result returned by an SQL query? Like attaching a ReviewsCount field equal to COUNT(Reviews.ID) as ReviewsCount.
I know it is possible to do that in a function like
public function getReviewsCount() {
return count($this->Reviews);
}
But I want doing this with SQL to minimize number of database queries and increase performance, as normally I may not need to load hundreds of reviews, but still need to know there number. I think running SQL's COUNT would be much faster than going through 100 Products and calculating 100 Reviews for each. Moreover, that is just example, on practice I need more complex functions, that I think MySQL would process faster. Correct me if I'm wrong.
You can map a single column result to an entity field - look at native queries and ResultSetMapping to achieve this. As a simple example:
use Doctrine\ORM\Query\ResultSetMapping;
$sql = '
SELECT p.*, COUNT(r.id)
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
';
$rsm = new ResultSetMapping;
$rsm->addEntityResult('AppBundle\Entity\Product', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');
$query = $this->getEntityManager()->createNativeQuery($sql, $rsm);
$results = $query->getResult();
Then in your Product entity you would have a $reviewsCount field and the count would be mapped to that. Note that this will only work if you have a column defined in the Doctrine metadata, like so:
/**
* #ORM\Column(type="integer")
*/
private $reviewsCount;
public function getReviewsCount()
{
return $this->reviewsCount;
}
This is what is suggested by the Aggregate Fields Doctrine documentation. The problem is here is that you are essentially making Doctrine think you have another column in your database called reviews_count, which is what you don't want. So, this will still work without physically adding that column, but if you ever run a doctrine:schema:update it's going to add that column in for you. Unfortunately Doctrine does not really allow virtual properties, so another solution would be to write your own custom hydrator, or perhaps subscribe to the loadClassMetadata event and manually add the mapping yourself after your particular entity (or entities) load.
Note that if you do something like COUNT(r.id) AS reviewsCount then you can no longer use COUNT(id) in your addFieldResult() function, and must instead use the alias reviewsCount for that second parameter.
You can also use the ResultSetMappingBuilder as a start into using the result set mapping.
My actual suggestion is to do this manually instead of going through all of that extra stuff. Essentially create a normal query that returns both your entity and scalar results into an array, then set the scalar result to a corresponding, unmapped field on your entity, and return the entity.
After detailed investigation I've found there are several ways to do something close to what I wanted including listed in other answers, but all of them have some minuses. Finally I've decided to use CustomHydrators. It seems that properties not managed with ORM cannot be mapped with ResultSetMapping as fields, but can be got as scalars and attached to an entity manually (as PHP allows to attach object properties on the fly). However, result that you get from doctrine remains in the cache. That means properties set in that way may be reset if you make some other query that would contain these entities too.
Another way to do that was adding these field directly to doctrine's metadata cache. I tried doing that in a CustomHydrator:
protected function getClassMetadata($className)
{
if ( ! isset($this->_metadataCache[$className])) {
$this->_metadataCache[$className] = $this->_em->getClassMetadata($className);
if ($className === "SomeBundle\Entity\Product") {
$this->insertField($className, "ReviewsCount");
}
}
return $this->_metadataCache[$className];
}
protected function insertField($className, $fieldName) {
$this->_metadataCache[$className]->fieldMappings[$fieldName] = ["fieldName" => $fieldName, "type" => "text", "scale" => 0, "length" => null, "unique" => false, "nullable" => true, "precision" => 0];
$this->_metadataCache[$className]->reflFields[$fieldName] = new \ReflectionProperty($className, $fieldName);
return $this->_metadataCache[$className];
}
However, that method also had problems with entities' properties reset. So, my final solution was just to use stdClass to get the same structure, but not managed by doctrine:
namespace SomeBundle;
use PDO;
use Doctrine\ORM\Query\ResultSetMapping;
class CustomHydrator extends \Doctrine\ORM\Internal\Hydration\ObjectHydrator {
public function hydrateAll($stmt, $resultSetMapping, array $hints = array()) {
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
$result = [];
foreach($resultSetMapping->entityMappings as $root => $something) {
$rootIDField = $this->getIDFieldName($root, $resultSetMapping);
foreach($data as $row) {
$key = $this->findEntityByID($result, $row[$rootIDField]);
if ($key === null) {
$result[] = new \stdClass();
end($result);
$key = key($result);
}
foreach ($row as $column => $field)
if (isset($resultSetMapping->columnOwnerMap[$column]))
$this->attach($result[$key], $field, $this->getPath($root, $resultSetMapping, $column));
}
}
return $result;
}
private function getIDFieldName($entityAlias, ResultSetMapping $rsm) {
foreach ($rsm->fieldMappings as $key => $field)
if ($field === 'ID' && $rsm->columnOwnerMap[$key] === $entityAlias) return $key;
return null;
}
private function findEntityByID($array, $ID) {
foreach($array as $index => $entity)
if (isset($entity->ID) && $entity->ID === $ID) return $index;
return null;
}
private function getPath($root, ResultSetMapping $rsm, $column) {
$path = [$rsm->fieldMappings[$column]];
if ($rsm->columnOwnerMap[$column] !== $root)
array_splice($path, 0, 0, $this->getParent($root, $rsm, $rsm->columnOwnerMap[$column]));
return $path;
}
private function getParent($root, ResultSetMapping $rsm, $entityAlias) {
$path = [];
if (isset($rsm->parentAliasMap[$entityAlias])) {
$path[] = $rsm->relationMap[$entityAlias];
array_splice($path, 0, 0, $this->getParent($root, $rsm, array_search($rsm->parentAliasMap[$entityAlias], $rsm->relationMap)));
}
return $path;
}
private function attach($object, $field, $place) {
if (count($place) > 1) {
$prop = $place[0];
array_splice($place, 0, 1);
if (!isset($object->{$prop})) $object->{$prop} = new \stdClass();
$this->attach($object->{$prop}, $field, $place);
} else {
$prop = $place[0];
$object->{$prop} = $field;
}
}
}
With that class you can get any structure and attach any entities however you like:
$sql = '
SELECT p.*, COUNT(r.id)
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
';
$em = $this->getDoctrine()->getManager();
$rsm = new ResultSetMapping();
$rsm->addEntityResult('SomeBundle\Entity\Product', 'p');
$rsm->addFieldResult('p', 'COUNT(id)', 'reviewsCount');
$query = $em->createNativeQuery($sql, $rsm);
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'SomeBundle\CustomHydrator');
$results = $query->getResult('CustomHydrator');
Hope that may help someone :)
Yes, it is possible, you need to use QueryBuilder to achieve that:
$result = $em->getRepository('AppBundle:Product')
->createQueryBuilder('p')
->select('p, count(r.id) as countResult')
->leftJoin('p.Review', 'r')
->groupBy('r.id')
->getQuery()
->getArrayResult();
and now you can do something like:
foreach ($result as $row) {
echo $row['countResult'];
echo $row['anyOtherProductField'];
}
If you're on Doctrine 2.1+, consider using EXTRA_LAZY associations:
They allow you to implement a method like yours in your entity, doing a straight count on the association instead of retrieving all the entities in it:
/**
* #ORM\OneToMany(targetEntity="Review", mappedBy="Product" fetch="EXTRA_LAZY")
*/
private $Reviews;
public function getReviewsCount() {
return $this->Reviews->count();
}
The previous answers didn't help me, but I found a solution doing the following:
My use case was different so the code is a mock. But the key is to use addScalarResult and then cleanup the result while setting the aggregate on the entity.
use Doctrine\ORM\Query\ResultSetMappingBuilder;
// ...
$sql = "
SELECT p.*, COUNT(r.id) AS reviewCount
FROM products p
LEFT JOIN reviews r ON p.id = r.product_id
";
$em = $this->getEntityManager();
$rsm = new ResultSetMappingBuilder($em, ResultSetMappingBuilder::COLUMN_RENAMING_CUSTOM);
$rsm->addRootEntityFromClassMetadata('App\Entity\Product', 'p');
$rsm->addScalarResult('reviewCount', 'reviewCount');
$query = $em->createNativeQuery($sql, $rsm);
$result = $query->getResult();
// APPEND the aggregated field to the Entities
$aggregatedResult = [];
foreach ($result as $resultItem) {
$product = $resultItem[0];
$product->setReviewCount( $resultItem["reviewCount"] );
array_push($aggregatedResult, $product);
}
return $aggregatedResult;
In my migration file, I gave my table pages a enum field with 2 possible values (as seen below). My question is, if it's possible to select these values with Laravels Eloquent?
$table->enum('status', array('draft','published'));
There are several Workarounds that I found, but there must be some "eloquent-native" way to handle this. My expected output would be this (that would be perfect!):
array('draft','published')
Thank you in advance!
Unfortunately, Laravel does not offer a solution for this. You will have to do it by yourself. I did some digging and found this answer
You can use that function and turn it into a method in your model class...
class Page extends Eloquent {
public static function getPossibleStatuses(){
$type = DB::select(DB::raw('SHOW COLUMNS FROM pages WHERE Field = "type"'))[0]->Type;
preg_match('/^enum\((.*)\)$/', $type, $matches);
$values = array();
foreach(explode(',', $matches[1]) as $value){
$values[] = trim($value, "'");
}
return $values;
}
}
And you use it like this
$options = Page::getPossibleStatuses();
If you want you can also make it a bit more universally accessible and generic.
First, create a BaseModel. All models should then extend from this class
class BaseModel extends Eloquent {}
After that, put this function in there
public static function getPossibleEnumValues($name){
$instance = new static; // create an instance of the model to be able to get the table name
$type = DB::select( DB::raw('SHOW COLUMNS FROM '.$instance->getTable().' WHERE Field = "'.$name.'"') )[0]->Type;
preg_match('/^enum\((.*)\)$/', $type, $matches);
$enum = array();
foreach(explode(',', $matches[1]) as $value){
$v = trim( $value, "'" );
$enum[] = $v;
}
return $enum;
}
You call this one like that
$options = Page::getPossibleEnumValues('status');
Made a small improvement to lukasgeiter's function. The foreach loop in his answer is parsing the string. You can update the regex to do that for you.
/**
* Retrieves the acceptable enum fields for a column
*
* #param string $column Column name
*
* #return array
*/
public static function getPossibleEnumValues ($column) {
// Create an instance of the model to be able to get the table name
$instance = new static;
// Pulls column string from DB
$enumStr = DB::select(DB::raw('SHOW COLUMNS FROM '.$instance->getTable().' WHERE Field = "'.$column.'"'))[0]->Type;
// Parse string
preg_match_all("/'([^']+)'/", $enumStr, $matches);
// Return matches
return isset($matches[1]) ? $matches[1] : [];
}
This throws an error if the column does not exist. So I added a small check in the code
public static function getPossibleEnumValues ($column) {
// Create an instance of the model to be able to get the table name
$instance = new static;
$arr = DB::select(DB::raw('SHOW COLUMNS FROM '.$instance->getTable().' WHERE Field = "'.$column.'"'));
if (count($arr) == 0){
return array();
}
// Pulls column string from DB
$enumStr = $arr[0]->Type;
// Parse string
preg_match_all("/'([^']+)'/", $enumStr, $matches);
// Return matches
return isset($matches[1]) ? $matches[1] : [];
}
As of L5.17 Eloquent does not include this functionality, instead you need to fall back to native QL. Here's an example that will work with SQL and in one line - returning an array like you asked.
In the spirit of one liner complexity ;)
I threw this in one of my view composers - it fetches the column from the table, explodes it and assembles the values in an array.
I iterate over that in my views using a foreach.
explode (
"','",
substr (
DB::select(" SHOW COLUMNS
FROM ".(new \Namespace\Model)->getTable()."
LIKE 'colName'"
)[0]->Type,
6,
-2
)
);
Could you help to extend a little bit about the Zend Quickstart: In the tutorial, we use the Mapper to update a single Guestbook. What if I want to update more than one Guestbook? And based on some conditions?
For example, I have an action to delete all Guestbooks that were created before 2012-12-21. What should I update to achieve that?
Does my approach make sense?
// application/models/GuestbookMapper.php
class Application_Model_GuestbookMapper
{
public function deleteByCreatedBefore($date)
{
$this->getDbTable()->deleteByCreatedBefore($date);
}
}
// application/models/DbTable/Guestbook.php
class Application_Model_DbTable_Guestbook extends Zend_Db_Table_Abstract
{
public function deleteByCreatedBefore($date) {
$where = $this->getAdapter()->quoteInto('created < ?', $date);
$this->delete($where);
}
}
Thanks,
If you are using the quickstart model/mapper and want to stay true to that data mapper paradigm you wouldn't have anything in your Application_Model_DbTable_Guestbook except for properties ('name', 'primary'...). The DbTable model would exist as the database adapter for that single table.
Your delete function would be placed in the mapper.
class Application_Model_GuestbookMapper
{
public function deleteByCreatedBefore($date)
{
$where = $this->getDbTable()->quoteInto('created < ?', $date);
//delete() returns num of rows deleted
$this->getDbTable()->delete($where);
}
}
This will work but may not be the best/safest way to achieve the required functionality.
This particular example of the Data Mapper is very simple and might be somewhat misleading to some people. The Guestbook example of the Mapper is really not a good representation of the mapper as the database row and the domain model (Application_Model_Guestbook) map 1 to 1 (one database column to one model property).
Where the Data Mapper starts to shine is when you need to map several database tables to a single Domain Model. With the understanding that your Domain Model (Application_Model_Guestbook) may have to effect more then one database table each time delete() is called, the structure for the delete() function is important.
What should you do to accomplish a delete with the mapper?
First: update Application_Model_GuestbookMapper::fetchAll() to accept a $where parameter, I usually setup this type of function to accept an array that sets the column and the value.
//accepted parameters: Zend_Db_Table::fetchAll($where = null, $order = null, $count = null, $offset = null)
//accepts array (column => value )
public function fetchAll(array $where = null)
{
$select = $this->getDbTable()->select();
if (!is_null($where) && is_array($where)) {
//using a column that is not an index may effect database performance
$select->where($where['column'] = ?, $where['value']);
}
$resultSet = $this->getDbTable()->fetchAll($select);
$entries = array();
foreach ($resultSet as $row) {
$entry = new Application_Model_Guestbook();
$entry->setId($row->id)
->setEmail($row->email)
->setComment($row->comment)
->setCreated($row->created);
$entries[] = $entry;
}
return $entries;
}
Second: Refactor your Application_Model_GuestbookMapper::deleteByCreatedBefore() to accept the output from fetchAll() (actually it would be simpler to just build a delete() function that accepts the output: array of Guestbook objects)
//accepts an array of guestbook objects or a single guestbook object
public function deleteGuestbook($guest)
{
if (is_array($guest) {
foreach ($guest as $book) {
if ($book instanceof Application_Model_Guest){
$where = $this->getDbTable()->quoteInto('id = ?', $book->id);
$this->getDbTable()->delete($where);
}
}
} elseif ($guest instanceof Application_Model_Guest) {
$where = $this->getDbTable()->quoteInto('id = ?', $guest->id);
$this->getDbTable()->delete($where);
} else {
throw new Exception;
}
}
Deleting a domain object as an object will become more important as you have to consider how deleting an object will affect other objects or persistence (database) paradigms. You will at some point encounter a situation where you don't want a delete to succeed if other objects still exist.
This is only an opinion but I hope it helps.
Is it possible to override values from Model->fetchAll() so it work globally. I have tried to override this in model, but does not work:
class Application_Model_DbTable_OdbcPush extends Zend_Db_Table_Abstract
{
public function __get(string $col)
{
$res = parent::__get($col);
if ($col == "lastrun") {
$res = ($res == "1912-12-12 00:00:00+07" ? NULL : $res);
}
return $res;
}
//...
}
In a controller:
$odbcModel = new Application_Model_DbTable_OdbcPush();
$rs = $odbcModel->fetchAll( $select );
I want to override value returned from fetchAll(), find() etc when col name is "lastrun";
The way you're going about this isn't going to work. __get is used to get data from protected or private properties and typically used in conjunction with getters.
For example, if you implemented __get() in your Application_Model_DbTable_OdbcPush class you could do something like:
$model = new Application_Model_DbTable_OdbcPush();
//echo out the _primary property (primary key of the table)
echo $model->primary;
and expect it to work. Because _primary exists as a property in Zend_Db_Table_Abstract.
To do what you want to do you'll need to do it after the result set has been returned (unless you want to rewrite the whole Zend Db component). Just run the result set through a foreach and change the value of lastrun to whatever you want.
I tried to find a place to override the Zend Db components to do what you want, but it would involve to many classes.
Remember that when using DbTable classes, they only interact with one table. You'll need to duplicate code for every table you want to effect or you'll need to extend a base class of some kind.
You always have the option to use straight Sql to frame whatever query you can come up with.
Good Luck!
Found the answer, for community i share here :D
http://framework.zend.com/manual/1.12/en/zend.db.table.row.html
So we have to overload Zend_Db_Table_Row and assign it to model/dbtable:
class Application_Model_DbTable_Row_OdbcPush extends Zend_Db_Table_Row_Abstract
{
// do some override here
}
class Application_Model_DbTable_OdbcPush extends Zend_Db_Table_Abstract
{
protected $_name = 'odbcpush';
protected $_primary = 'id';
private $_global = null;
protected $_rowClass = "Application_Model_DbTable_Row_OdbcPush";
// etc
}
I'm dealing with database containing of many tables, with many field prefixes (two first letters of every table), so when I have users table I cannot use "name" property ($user->name) but I can use: $user->us_name.
I's there a way to simplify things and set automagic prefix for every field of a table ?
You'd have to extend Zend_Db_Table_Row to accomplish this. Fortunately, ZF includes a _transformColumn() method expressly for this purpose. But I'm getting ahead of myself. First, set up your table class. This assumes your database has a table called "foo_mytable":
class MyTable extends Zend_Db_Table_Abstract {
protected $_name = 'foo_mytable';
protected $_rowClass = 'My_Db_Table_Row';
}
Next, create your custom Row class:
class My_Db_Table_Row extends Zend_Db_Table_Row {
protected function _transformColumn($columnName) {
$columnName = parent::_transformColumn($columnName);
$prefix = 'us_';
return $prefix . $columnName;
}
}
Now, you can do something like this (for simplicity, this example ignores MVC design ideals):
$table = new MyTable();
$records = $table->fetchAll();
foreach ($records as $record) {
echo $record->name;
}
Assuming your table has a column named "us_name", this should work. I tested it myself. Note that in your custom table row, you might want to grab the table prefix from a config file. If you've got it stored in your registry, you could replace $prefix = 'us_'; with $prefix = Zend_Registry::get('tablePrefix');.
I didn't know about _transformColumn(). I'm gonna hop on top of #curtisdf 's example.
I think you should override with this (not tested):
protected function _transformColumn($columnName)
{
$tblName = $this->_table->info(Zend_Db_Table::NAME);
$prefix = substr($tblName, 0, 2);
return $prefix . '_' . parent::_transformColumn($columnName);
}
Using this, you won't need to store prefixes/table-names, as they are retrieved dinamically.