Doctrine: addSelect method creates undesired seperate object (Symfony) - mysql

I want to query the entity based on a calculated property that does not exist in the database or on the entity.
If I run
return $this->createQueryBuilder('b')
->select('b')
->addSelect(
'... as extra_property'
)
->having('extra_property = :param')
->setParameter('param', $param)
->orderBy('extra_property', 'ASC')
->getQuery()
->getResult();
This results in a collection with each entity in the following format:
"0": {}, // The 9 entity properties
"extra_property": "value"
However, I want the extra_property to be added to the other entity properties as the tenth property. How do I fix this?

The problem lies in the getResult() method, the default hydration method is "HYDRATE_OBJECT" which will try to hydrate the output as the defined entity, which does not know about your extra property.
public function getResult($hydrationMode = self::HYDRATE_OBJECT)
{
return $this->execute(null, $hydrationMode);
}
Depending on what your other properties are, you could just use the "HYDRATE_SCALAR" option which will just give you a flat output of the results.
If you other properties are nested entities, you will have to manually select the fields in your select to bypass the hydration process, or find a way to add that property to your entity and tell the hydrator to get that data from a dedicated query or something.
You can find the different hydration methods as constants of the AbstractQuery class, you can read more on the Doctrine documentation
/**
* Hydrates an object graph. This is the default behavior.
*/
public const HYDRATE_OBJECT = 1;
/**
* Hydrates an array graph.
*/
public const HYDRATE_ARRAY = 2;
/**
* Hydrates a flat, rectangular result set with scalar values.
*/
public const HYDRATE_SCALAR = 3;
/**
* Hydrates a single scalar value.
*/
public const HYDRATE_SINGLE_SCALAR = 4;
/**
* Very simple object hydrator (optimized for performance).
*/
public const HYDRATE_SIMPLEOBJECT = 5;
/**
* Hydrates scalar column value.
*/
public const HYDRATE_SCALAR_COLUMN = 6;

Related

symfony doctrine will not default a boolean to 0

I have tried in several ways to have symfony default a boolean to 0 rather than null (as null gives me a database level error upon flush).
An exception occurred while executing a query: SQLSTATE[23000]:
Integrity constraint violation: 1048 Column 'auto_created' cannot be
null
This made no difference:
/**
* #ORM\Column(type="boolean", options={"default":"0"})
*/
private $autoCreated;
Some logic i the setter made no difference either
public function setAutoCreated(bool $autoCreated): self
{
if is_null($autoCreated) {
$autoCreated = 0;
}
$this->autoCreated = $autoCreated;
return $this;
}
As well as
public function setAutoCreated(bool $autoCreated): self
{
if is_null($autoCreated) {
$autoCreated = false;
}
$this->autoCreated = $autoCreated;
return $this;
}
Database looks like this
I am clearly missing something...?
Sure I can do a simple $user->setAutoCreated(false); everywhere I create this entity, but I don't get why I should have to 😎
Depending on the version of PHP you're using, you should be able to do something like this in your entity class:
/**
* #ORM\Column(type="boolean")
*/
private $autoCreated = false;
Whenever an instance of this class is created, $autoCreated will be set to false. So when you try to persist the object, it'll have a default value of false and Doctrine will set the field to 0.
Alternatively, you can explicitly set $autoCreated to false in your constructor:
public function __construct()
{
$this->autoCreated = false;
}
Note that you can only use the first approach for simple, built-in PHP types or constants. For more complicated objects (e.g., a Doctrine ArrayCollection) you'll need to use the constructor approach.

Yii2 relation based on attribute values instead of keys

I have 2 tables in the db (mysql), and between the 2 there is no classic relationship through keys or ids. The only way I could define relationship would be through attribute values. E.g. table wheel and car and certain wheels would match certain cars because of the size only. Can it be defined on DB level, and/or in yii2, and if yes, how?
In the relations I can add an onCondition(), but you have to define an attribute (???), too:
public function getWheels() {
return $this->hasMany(\app\models\Wheel::className(), ['???' => '???'])->onCondition(['<', 'wheelsize', $this->wheelsize]);
}
I could use a fake attribute and set it in all records like to 1, but it seems a little bit odd for me.
I find nothing on the web regarding this or maybe I'm just searching the wrong way, or maybe I'm trying something that's totally bad practice. Can you please point me to the right direction?
Hypothetically you can set an empty array as a link, but for security reasons (I think) the condition "0 = 1" is automatically added in the select.
I faced your own problem several times and the best solution I could find was to use ActiveQuery explicitly (similar to what happens for hasOne and hasMany):
public function getWheels() {
return new ActiveQuery(\app\models\Wheel::className(), [
'where' => 'my condition' // <--- inserte here your condition as string or array
'multiple' => true // true=hasMany, false=hasOne
// you can also add other configuration params (select, on condition, order by, ...
]);
}
This way you can get both the array and the ActiveQuery to add other conditions:
var_dump($model->wheels); // array of wheels objects
var_dump($model->getWheels()); // yii\db\ActiveQuery object
$model->getWheels()->andWhere(...); // customize active query
I don't think that you could achieve this through relation.
But there is a way to work around the limitation.
<?php
namespace app\models;
class Car extend \yii\db\ActiveRecord
{
/**
* #var \app\models\Wheel
*/
private $_wheels;
/**
* #return \app\models\Wheel[]
*/
public function getWheels()
{
if (!$this->_wheels) {
$this->_wheels = Wheel::find()
->where(['<', 'wheelsize', $this->wheelsize])
//->andWhere() customize your where here
->all();
}
return $this->_wheels;
}
}
Then you could access the wheels attribute just as relation does.
<?php
$car = Car::find(1);
$car->wheels;
Beware that this way does not support Eager Loading

Doctrine findBy boolean field returns no results

Recently, a piece of code stopped working. I haven't made any changes to it so I don't know why.
Here's the code:
$invites = $this->vault_em->getRepository('AppBundle:Invite\LocalInvite')->findBy([
'active' => true,
]);
Now, it's returning an empty array, even though there are LocalInvite records with active = 1.
Here are the doctrine mappings:
/**
* #ORM\Entity
* #ORM\Table(name="invite")
*/
class LocalInvite extends Invite {
//...
}
/** #ORM\MappedSuperclass */
abstract class Invite implements \JsonSerializable {
/** #ORM\Column(type="boolean", options={"default": true}) */
protected $active;
//...
}
To debug, I copied the underlying MySQL query that Doctrine is executing from the debug logs:
SELECT t0.id AS id_1, t0.email AS email_2, t0.active AS active_3, t0.location AS location_4, t0.cohort_leadership AS cohort_leadership_5, t0.timezone AS timezone_6, t0.date_record_created AS date_record_created_7, t0.date_record_deleted AS date_record_deleted_8, t0.date_restart AS date_restart_9, t0.date_start_invite AS date_start_invite_10, t0.employee_id AS employee_id_11, t0.first_name AS first_name_12, t0.corporate_client_name AS corporate_client_name_13, t0.client_id AS client_id_14, t0.landing_page_url AS landing_page_url_15, t0.user_id AS user_id_16, t0.recipient_id AS recipient_id_17 FROM invite t0 WHERE t0.active = true;
When I plug that query into a MySQL IDE, it returns results.
Why does the findBy return no results?
try to change 'AppBundle:Invite\LocalInvite' by LocalInvite::class

Yii2 eager load aggregation through junction table

I have the following tables:
content - id (PK), title, ... other fields ...
content_category - content_id (FK to content), category_id (FK to content)
Where a piece of content has_many categories, and a category is also a piece of content.
In content I have the following code:
public function getCategories()
{
return $this
->hasMany(Category::className(), ['id' => 'category_id'])
->viaTable('content_category', ['content_id' => 'id']);
}
public function getCategoriesCsv(){
...
}
For my grid view in the backend, I'd like to display a comma separated list of categories for each piece of content.
I'm aware that I could select this information separately, but I would like to do it as part of the find query and using the existing relation if possible.
Using defined relation (more simple, less efficient).
This approach typical way and it works with related Category models. Therefore it requires a lot of memory.
class Content extends \yii\db\ActiveRecord
{
/**
* Returns comma separated list of category titles using specified separator.
*
* #param string $separator
*
* #return string
*/
public function getCategoriesCsv($separator = ', ')
{
$titles = \yii\helpers\ArrayHelper::getColumn($this->categories, 'title');
return implode($separator, $titles);
}
// ...
}
Should be used with eager loading:
Content::find()
->with('categories')
->all();
Using subquery (more efficient, less convenient)
This approach uses subqueries and don't use relations and related models. Therefore this way more fast and keeps a lot of memory.
class Content extends \yii\db\ActiveRecord
{
const ATTR_CATEGORIES_CSV = 'categoriesCsv';
/**
* #var string Comma separated list of category titles.
*/
public $categoriesCsv;
/**
* Returns DB expression for retrieving related category titles.
*
* #return \yii\db\Expression
*/
public function prepareRelatedCategoriesExpression()
{
// Build subquery that selects all category records related with current content row.
$queryRelatedCategories = Category::find()
->leftJoin('{{%content_category}}', '{{%content_category}}.[[category_id]] = {{%category}}.[[id]]')
->andWhere(new \yii\db\Expression('{{%content_category}}.[[content_id]] = {{%content}}.[[id]]'));
// Prepare subquery for retrieving only comma-separated titles
$queryRelatedCategories
->select(new \yii\db\Expression('GROUP_CONCAT( {{%category}}.[[title]] )'));
// Prepare expression with scalar value from subquery
$sqlRelatedCategories = $queryRelatedCategories->createCommand()->getRawSql();
return new \yii\db\Expression('(' . $sqlRelatedCategories . ')');
}
// ...
}
When alias of additional column equals to some model property, it will be populated by all() method:
$contentModels = Content::find()
->andSelect([
'*',
Content::ATTR_CATEGORIES_CSV => Content::prepareRelatedCategoriesExpression(),
])
->all();
foreach ($contentModels as $contentModel) {
$contentModel->id;
$contentModel->categoriesCsv; // it will be also populated by ->all() method.
// ...
}
ps: I not tested this code, probably should be fixed query for retrieving categories.
Moreover, in this example it written using base simple syntax, but it may be optimized to more cute state using various helpers, junction models etc.
WITH CATEGORIES LOADED
Originally I implemented it as:
public function getCategoriesCsv(){
$categoryTitles = [];
foreach ($this->categories as $category){
$categoryTitles[] = $category->title;
}
return implode(', ', $categoryTitles);
}
Thanks to #IStranger, I neatened this to:
public function getCategoriesCsv()
{
$titles = ArrayHelper::getColumn($this->categories, 'title');
return implode(', ', $titles);
}
WITHOUT CATEGORIES LOADED
I have now managed to avoid loading all the category models by adding a separate CategoryCsv ActiveRecord:
class CategoryCsv extends ActiveRecord
{
public static function tableName(){
return '{{%content_category}}';
}
public function attributes(){
return ['content_id', 'value'];
}
public static function find(){
return parent::find()
->select([
'content_id',
'GROUP_CONCAT(
categoryCsv.title
ORDER BY categoryCsv.title
SEPARATOR ", "
) value'
])
->innerJoin('content categoryCsv','category_id = categoryCsv.id')
->groupBy('content_id');
}
}
Then in the Content ActiveRecord:
public function getCategoriesCsv(){
return $this->hasOne(CategoryCsv::className(), ['content_id' => 'id']);
}
Thus I can access the value like so:
$contents = Content::find()->with('categoryCsv')->all();
foreach($contents as $content){
echo $content->categoryCsv->value;
}

Symfony2 tree builder - what does the method canBeUnset() do?

$rootNode
->children()
->arrayNode('form')
->info('form configuration')
->canBeUnset()
->treatNullLike(array('enabled' => true))
->treatTrueLike(array('enabled' => true))
->children()
->booleanNode('enabled')->defaultTrue()->end()
->end()
->end()
Line 5 of the above snippet from Symfony\Bundle\FrameworkBundle\DependencyInjection\Configuration uses the method canBeUnset(). I don't know what this does because it seems to not do anything if I remove it. I'm working understanding semantic configuration for my own bundles.
Following the code, you can find definition for this method in Symfony/Component/Config/Definition/Builder/ArrayNodeDefinition class.
/**
* Sets whether the node can be unset.
*
* #param Boolean $allow
*
* #return ArrayNodeDefinition
*/
public function canBeUnset($allow = true)
{
$this->merge()->allowUnset($allow);
return $this;
}
This is passed to MergeBuilder ( Symfony/Component/Config/Definition/Builder/MergeBuilder ) which handles config merging.
/**
* Sets whether the node can be unset.
*
* #param Boolean $allow
*
* #return MergeBuilder
*/
public function allowUnset($allow = true)
{
$this->allowFalse = $allow;
return $this;
}
So my understanding is, that this method defines, if your config value can be unset while merging configurations, in case the overriding config does not support the value. I would have to test though, to find out the behaviour if the unsetting is not allowed, but I guess then it would throw an exception about a missing config value just like isRequired.