In my repositories, I have methods with too many arguments (for use in where) :
Example :
class ProchaineOperationRepository extends EntityRepository
{
public function getProchaineOperation(
$id = null, // Search by ID
\DateTime $dateMax = null, // Search by DateMax
\DateTime $dateMin = null, // Search by DateMin
$title = null // Search by title
)
In my controllers, I have differents action ... for get with ID, for get with ID and DateMin, for get ID and Title, ...
My method is too illegible because too many arguments ... and it would be difficult to create many methods because they are almost identical ...
What is the best practice ?
You have two main concerns in your question
You have too many arguments in your repository method which will be used in 'where' condition of the eventual query. You want to organize them in a better way
The repository method should be callable from the controller in a meaningful way because of possible complexity of arguments passed
I suggest you to write a Repository method like:
namespace AcmeBundle\Repository;
/**
* ProchaineOperationRepository
*
*/
class ProchaineOperationRepository extends \Doctrine\ORM\EntityRepository
{
public function search($filters, $sortBy = "id", $orderBy = "DESC")
{
$qb = $this->createQueryBuilder("po");
foreach ($filters as $key => $value){
$qb->andWhere("po.$key='$value'");
}
$qb->addOrderBy("po.$sortBy", $orderBy);
return $qb->getQuery()->getArrayResult();
}
}
The $filters variable here is an array which is supposed to hold the filters you are going to use in 'where' condition. $sortBy and $orderBy should also be useful to get the result in properly sequenced way
Now, you can call the repository method from your controller like:
class ProchaineOperationController extends Controller
{
/**
* #Route("/getById/{id}")
*/
public function getByIdAction($id)
{
$filters = ['id' => $id];
$result = $this->getDoctrine()->getRepository("AcmeBundle:ProchaineOperation")->search($filters);
//process $result
}
/**
* #Route("/getByTitle/{title}")
*/
public function getByTitleAction($title)
{
$filters = ['title' => $title];
$sortBy = 'title';
$result = $this->getDoctrine()->getRepository("AcmeBundle:ProchaineOperation")->search($filters, $sortBy);
//process $result
}
/**
* #Route("/getByIdAndDateMin/{id}/{dateMin}")
*/
public function getByIdAndDateMinAction($id, $dateMin)
{
$filters = ['id' => $id, 'dateMin' => $dateMin];
$sortBy = "dateMin";
$orderBy = "ASC";
$result = $this->getDoctrine()->getRepository("AcmeBundle:ProchaineOperation")->search($filters, $sortBy, $orderBy);
//process $result
}
}
Note that you are calling the same repository method for all controller actions with minor changes according to your parameters. Also note that $sortBy and $orderBy are optionally passed.
Hope it helps!
If your objective is only to query with an AND operator between each properties, the best way could be to use the method proposed by doctrine for that : findBy() cf : this part of the doc
for instance :
$results = $this
->getDoctrine()
->getRepository('AppBundle:ProchaineOperation')
->findBy(array('dateMax' => $myDate, 'title' => 'Hello world');
EDIT : after comment
Then use the same way as Doctrine do : Pass only an array with id, dateMax... as keys if these are set. This should be solve the method signature problem which gives you so much trouble. :)
Related
Let's say you have this relationship: users x cats. Each user can have many cats (a "one-to-many" relationship):
class Cat extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
Both models (users and cats) have a name field.
Let's say we want to get all cats with bob in their names, using Laravel's Scout.
The standard solution is to add this to the Cat.php model:
// Cat.php
use Searchable;
/**
* Get the indexable data array for the model.
*
* #return array
*/
public function toSearchableArray()
{
return [
'name' => $this->name,
];
}
And we search with Cat::search('bob')->get().
The problem
The above solution works well, but what if we want to search in the relationship's fields?
What if you want to get cats owned by people with bob in their names?
If you add this to the "Cat" model:
// Cat.php
use Searchable;
/**
* Get the indexable data array for the model.
*
* #return array
*/
public function toSearchableArray()
{
return [
'name' => $this->name,
'users.name' => '', // no need to return `$this->user->name` as the database engine only uses the array keys
];
}
It won't work. You will get this exception when running Cat::search('bob')->get():
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'users.name' in 'where clause'
SQL: select `cats`.* from `cats` where (`cats`.`name` like %bob% or `users`.`name` like %bob%)
Clearly, the SQL is missing the users table. But how to add it? Doing a Cat::join(...)->search('bob') will throw an exception, same for Cat::search(...)->join(...).
The question is: How to search in the parent attributes? And by "parent" I mean the "belongsTo" model.
The query method allows for modifing the search query. Use it to inject a join clause:
Cat::search('bob')->query(function ($builder) {
$builder->select('cats.*')->join('users', 'cats.user_id', '=', 'users.id');
})->get();
This generates the proper query:
SELECT `cats`.*
FROM `cats`
INNER JOIN `users` on `cats`.`genre_id` = `users`.`id`
WHERE (`cats`.`name` LIKE '%bob%' or `users`.`name` LIKE '%bob%')
ORDER BY `id` desc
EDIT: Automatically adds the JOIN clause to all searches:
If you want to search with just Cat::search('bob')->get(), without having to write ->join(...) on every call:
// Cat.php
/**
* Overrides the "search" method to inject a `join` to the relationships.
*/
use Searchable {
Searchable::search as parentSearch;
}
/**
* Perform a search against the model's indexed data.
*
* #param string $query
* #param \Closure $callback
* #return \Laravel\Scout\Builder
*/
public static function search($query = '', $callback = null)
{
return static::parentSearch($query, $callback)->query(function ($builder) {
$builder->select('cats.*')->join('users', 'cats.user_id', '=', 'users.id');
});
}
I'm a beginner with Doctrine ORM (v2.5.5) and Silex (v2.0.4)/Symfony (v3.1.6). I need to output my Date field to the YYYY-MM-DD format. Let's say I have this annotation and getter method on my Entity:
// src/App/Entity/Tnkb.php (simplified)
// 'expire' field
/**
* #ORM\Column(type="date")
*/
protected $expire;
// getter
public function getExpire()
{
return !is_object($this->expire) ? new \DateTime() : $this->expire->format('Y-m-d');
}
Here's my simplified controller for debugging purpose:
$app->get('/debug', function() use ($app) {
$tnkbRepo = $app['orm.em']->getRepository('\App\Entity\Tnkb');
$normalizer = new \Symfony\Component\Serializer\Normalizer\ObjectNormalizer();
$encoder = new \Symfony\Component\Serializer\Encoder\JsonEncoder();
$normalizer->setCircularReferenceHandler(function($obj){
return $obj->getId();
});
$serializer = new \Symfony\Component\Serializer\Serializer(array($normalizer), array($encoder));
$qb = $tnkbRepo->createQueryBuilder('c')
->setMaxResults(1);
//$query = $qb->getQuery(); // [1] <<-- this line produce proper YYYY-MM-DD format
//$query = $qb->select('c.expire')->getQuery(); // [2] <<-- this (manual select) line produce DateTime object.
$results = $query->getResult();
return $serializer->serialize($results, 'json');
});
With the first [1] line uncommented I got the proper output I wanted:
[more json output here]...,"expire":"1970-10-25",...
But with the second [2] line uncommented (I intendedly omitted other fields for testing) I got the following output, which wasn't what I expected:
[{"expire":{"timezone":{"name":"UTC","location":{"country_code":"??","latitude":0,"longitude":0,"comments":""}},"offset":0,"timestamp":25660800}}]
I also noticed, with the [2] line Doctrine seems to ignore my entity's getter method (I tried returning empty string). I expect the output will be the same as the [1] case, it makes me curious. My questions are:
How do I achieve the same proper YYYY-MM-DD format with the [2] version?
And why are they produce different output format?
Thank you.
UPDATE
More simplified /debug controller for testing (no serialization):
$app->get('/debug', function() use ($app) {
$tnkbRepo = $app['orm.em']->getRepository('\App\Entity\Tnkb');
$qb = $tnkbRepo->createQueryBuilder('c');
// [1a] normal query. doesn't return Entity, getExpire() isn't called.
/*$query = $qb->select('c.expire')
->setMaxResults(1)->getQuery();*/
// [2a] partial query. returns Entity, getExpire() called.
/*$query = $qb->select('partial c.{id,expire}')
->setMaxResults(1)->getQuery();*/
$results = $query->getResult();
var_dump($results);die;
});
Updated Entity method getExpire():
// src/App/Entity/Tnkb.php (simplified)
// 'expire' field
/**
* #ORM\Column(type="date")
*/
protected $expire;
protected $dateAsString = true;
protected $dateFormat = 'Y-m-d';
// getter
public function getExpire()
{
return ($this->expire instanceof \DateTime) ? $this->dateOutput($this->expire)
: $this->dateOutput(new \DateTime());
}
protected function dateOutput(\DateTime $date) {
if ($this->dateAsString) {
return $date->format($this->dateFormat);
}
return $date;
}
Controller dump results:
[1a] normal query:
// non-entity
array(1) { [0]=> array(1) { ["expire"]=> object(DateTime)#354 (3) { ["date"]=> string(26) "1970-10-25 00:00:00.000000" ["timezone_type"]=> int(3) ["timezone"]=> string(3) "UTC" } } }
[2a] partial object query:
// array of entity
array(1) { [0]=> object(App\Entity\Tnkb)#353 (23) { /* more properties */...["expire":protected]=> object(DateTime).../* more properties */
I found out this is normal behaviour with Doctrine, it has something to do with Partial Objects. See my comment below. Link: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/partial-objects.html
I don't think it's good practice to return a \DateTime sometimes but a formatted string other times. However maybe you have your reasons for doing this.
The only reason I can think of the difference in results is if Doctrine calls getters on the properties when loading an entity. I tested with a simple class which has the same expire property and getter. Returning the class still had the serialized (not formatted) \DateTime object, which would suggest that at some point your getter is being called and the property set to a new \DateTime.
My recommendation is to look at the DateTimeNormalizer provided by Symfony in 3.1. If you can't upgrade to 3.1 then you can easily build your own one. Then you can be sure you'll always have consistent \DateTime format in all your responses. You can all remove the ->format(...) from your getter then and always return a \DateTime object. I think this is a much cleaner approach.
I currently have a fairly complex native SQL query which is used for reporting purposes. Given the amount of data it processes this is the only efficient way to handle it is with native SQL.
This works fine and returns an array of arrays from the scalar results.
What I'd like to do, to keep the results consistent with every other result set in the project is use a Data Transfer Object (DTO). Returning an array of simple DTO objects.
These work really well with DQL but I can't see anyway of using them with native SQL. Is this at all possible?
Doctrine can map the results of a raw SQL query to an entity, as shown here:
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/native-sql.html
I cannot see support for DTOs unless you are willing to use DQL as well, so a direct solution does not exist. I tried my hand at a simple workaround that works well enough, so here are the DQL and non-DQL ways to achieve your goal.
The examples were built using Laravel and the Laravel Doctrine extension.
The DTO
The below DTO supports both DQL binding and custom mapping so the constructor must be able to work with and without parameters.
<?php namespace App\Dto;
/**
* Date with corresponding statistics for the date.
*/
class DateTotal
{
public $taskLogDate;
public $totalHours;
/**
* DateTotal constructor.
*
* #param $taskLogDate The date for which to return totals
* #param $totalHours The total hours worked on the given date
*/
public function __construct($taskLogDate = null, $totalHours = null)
{
$this->taskLogDate = $taskLogDate;
$this->totalHours = $totalHours;
}
}
Using DQL to fetch results
Here is the standard version, using DQL.
public function findRecentDateTotals($taskId)
{
$fromDate = new DateTime('6 days ago');
$fromDate->setTime(0, 0, 0);
$queryBuilder = $this->getQueryBuilder();
$queryBuilder->select('NEW App\Dto\DateTotal(taskLog.taskLogDate, SUM(taskLog.taskLogHours))')
->from('App\Entities\TaskLog', 'taskLog')
->where($queryBuilder->expr()->orX(
$queryBuilder->expr()->eq('taskLog.taskLogTask', ':taskId'),
$queryBuilder->expr()->eq(0, ':taskId')
))
->andWhere(
$queryBuilder->expr()->gt('taskLog.taskLogDate', ':fromDate')
)
->groupBy('taskLog.taskLogDate')
->orderBy('taskLog.taskLogDate', 'DESC')
->setParameter(':fromDate', $fromDate)
->setParameter(':taskId', $taskId);
$result = $queryBuilder->getQuery()->getResult();
return $result;
}
Support for DTOs with native SQL
Here is a simple helper that can marshal the array results of a raw SQL query into objects. It can be extended to do other stuff as well, perhaps custom updates and so on.
<?php namespace App\Dto;
use Doctrine\ORM\EntityManager;
/**
* Helper class to run raw SQL.
*
* #package App\Dto
*/
class RawSql
{
/**
* Run a raw SQL query.
*
* #param string $sql The raw SQL
* #param array $parameters Array of parameter names mapped to values
* #param string $className The class to pack the results into
* #return Object[] Array of objects mapped from the array results
* #throws \Doctrine\DBAL\DBALException
*/
public static function query($sql, $parameters, $className)
{
/** #var EntityManager $em */
$em = app('em');
$statement = $em->getConnection()->prepare($sql);
$statement->execute($parameters);
$results = $statement->fetchAll();
$return = array();
foreach ($results as $result) {
$resultObject = new $className();
foreach ($result as $key => $value) {
$resultObject->$key = $value;
}
$return[] = $resultObject;
}
return $return;
}
}
Running the raw SQL version
The function is used and called in the same way as other repository methods, and just calls on the above helper to automate the conversion of data to objects.
public function findRecentDateTotals2($taskId)
{
$fromDate = new DateTime('6 days ago');
$sql = "
SELECT
task_log.task_log_date AS taskLogDate,
SUM(task_log.task_log_hours) AS totalHours
FROM task_log task_log
WHERE (task_log.task_log_task = :taskId OR :taskId = 0) AND task_log.task_log_date > :fromDate
GROUP BY task_log_date
ORDER BY task_log_date DESC
";
$return = RawSql::query(
$sql,
array(
'taskId' => $taskId,
'fromDate' => $fromDate->format('Y-m-d')
),
DateTotal::class
);
return $return;
}
Notes
I would not dismiss DQL too quickly as it can perform most kinds of SQL. I have however also recently been involved in building management reports, and in the world of management information the SQL queries can be as large as whole PHP files. In that case I would join you and abandon Doctrine (or any other ORM) as well.
I have simple question,
how can I create form list elements, something like grid or this:
[x] name | image | [button]
[ ] name | image | [button]
[x] name | image | [button]
<table>
<tr><th>checkbox</th><th>name</th><th>action</th></tr>
<tr><td><input type="checkbox"></td><td>name</td><td><button>OK</td></tr>
<tr><td><input type="checkbox"></td><td>name</td><td><button>OK</td></tr>
<tr><td><input type="checkbox"></td><td>name</td><td><button>OK</td></tr>
</table>
//list entities from db, array(object,object,object)
//object = Application\Entity\Area
$areas = $this->getObjectManager()->getRepository('Application\Entity\Area')->findAll();
I used in form Zend\Form\Element\Collection but I don't know how populate collection date from db, so I had clear form.
I should do it properly and what to use?
From Doctrine you already get an iterable datatype (array). So you only need to iterate it in your view:
...
<?php foreach($this->data as $area): ?>
//your table row markup for a single entity
<?php endforeach; ?>
...
Disclaimer: I have asked a similar question, with no answer. So I would also be keen to know the 'Zend' way or if anyone is able to suggest an alternative.
The approach below seems to work for me.
ListForm.php
Add a collection to your 'list' form.
/** The collection that holds each element **/
$name = $this->getCollectionName();
$collectionElement = new \Zend\Form\Element\Collection($name);
$collectionElement->setOptions(array(
'count' => 0,
'should_create_template' => false,
'allow_add' => true
));
$this->add($collectionElement);
This collection will hold out collection element (Zend\Form\Element\Checkbox)
/** The element that should be added for each item **/
$targetElement = new \Zend\Form\Element\Checkbox('id');
$targetElement->setOptions(array(
'use_hidden_element' => false,
'checked_value' => 1,
));
$collectionElement->setTargetElement($targetElement);
Then I add a few methods to allow me to pass an ArrayCollecion to the form. For each entity in my collection I will create a new $targetElement; setting its it's checked value to the id of the entity.
/**
* addItems
*
* Add multiple items to the collection
*
* #param Doctrine\Common\Collections\Collection $items Items to add to the
* collection
*/
public function addItems(Collection $items)
{
foreach($items as $item) {
$this->addItem($item);
}
return $this;
}
/**
* addItem
*
* Add a sigle collection item
*
* #param EntityInterface $entity The entity to add to the
* element collection
*/
public function addItem(EntityInterface $item)
{
$element = $this->createNewItem($item->getId());
$this->get($this->getCollectionName())->add($element);
}
/**
* createNewItem
*
* Create a new collection item
*
* #param EntityInterface $entity The entity to create
* #return \Zend\Form\ElementInterface
*/
protected function createNewItem($id, array $options = array())
{
$element = clone $this->targetElement;
$element->setOptions(array_merge($element->getOptions(), $options));
$element->setCheckedValue($id);
return $element;
}
All that is then needed is to pass the collection to the form from within the controller action.
SomeController
public function listAction()
{
//....
$users = $objectManager->getRepository('user')->findBy(array('foo' => 'bar'));
$form = $this->getServiceLocator()->get('my_list_form');
$form->addItems($users);
//...
}
You can populate multi-select checkbox using doctrine from the database using DoctrineModule\Form\Element\ObjectMultiCheckbox as in this page:
https://github.com/doctrine/DoctrineModule/blob/master/docs/form-element.md
simply you need to pass the entity manager to the form, and then do same as in the example you can create ObjectMultiCheckbox form element...
or the other better -moro automated work- method, if you want to use the collection you need to do the mapping right (#orm\OneToMany and #orm\ManyToOne) with the area... and the create a fieldset in the form as in here...:
http://framework.zend.com/manual/2.2/en/modules/zend.form.collections.html
and add methods to the other entity to add and remove the areas as this:
public function addArea(Collection $areas)
{
foreach ($areas as $area) {
$area->setOtherEntity($this);
$this->areas->add($area);
}
}
public function removeAreas(Collection $areas)
{
foreach ($areas as $area) {
$area->setOtherEntity(null);
$this->areas->removeElement($area);
}
}
By this if you use the hydration the values will be added and removed as you select them automatically...
I am using Docrine 1.2 with Zend Framework and trying to save a Doctrine Collection.
I am retrieving my collection from my table class with the following code.
public function getAll()
{
return $this->createQuery('e')
->orderBy('e.order ASC, e.eventType ASC')
->execute();
}
I also have the following class to reorder the above event records.
class Admin_Model_Event_Sort extends Model_Abstract
{
/**
* Events collection
* #var Doctrine_Collection
*/
protected $_collection = null;
public function __construct()
{
$this->_collection = Model_Doctrine_EventTypesTable::getInstance()->getAll();
}
public function save($eventIds)
{
if ($this->_collection instanceof Doctrine_Collection) {
foreach ($this->_collection as $record)
{
$key = array_search($record->eventTypeId, $eventIds);
if ($key !== false) {
$record->order = (string)$key;
}
}
return $this->_saveCollection($this->_collection);
} else {
return false;
}
}
}
The _saveCollection method above is as follows
/**
* Attempts to save a Doctrine Collection
* Sets the error message property on error
* #param Doctrine_Collection $collection
* #return boolean
*/
protected function _saveCollection(Doctrine_Collection $collection)
{
try {
$collection->save();
return true;
} catch (Exception $e) {
$this->_errorMessage = $e->getMessage();
OpenMeetings_Logger_ErrorLogger::write('Unable to save Doctrine Collection');
OpenMeetings_Logger_ErrorLogger::vardump($this->_errorMessage);
return false;
}
}
The event id's in the above save method is simply an enumerated array of event id's, I am using the keys of the array to set the sort order of the events using the order field. If I do a var_dump of the collection to an array ($this->_collection->toArray()) I get the correct data. However when I attempt to save the collection I get the following error.
"SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'order = '0' WHERE eventtypeid = '3'' at line 1"
Is there anyway I can get Doctrine to expand on this error, the full SQL statement would be a start, also if anyone knows as to why this error is occuring then that would be very helpful.
Many thanks in advance
Garry
EDIT
I have modified my above code to try to work one record at a time but I still get the same problem.
public function save($eventIds)
{
foreach ($eventIds as $key => $eventId) {
$event = Model_Doctrine_EventTypesTable::getInstance()->getOne($eventId);
$event->order = (string)$key;
$event->save();
}
}
Ok I have found the problem. I was using the MYSQL reserved word order as a field name thus the error, changed it to sortOrder and the problem went away.
Hope this helps someone with a similar issue.
Garry