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.
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 trying to create an initial DB state in DB Unit like this...
public function getDataSet() {
$primary = new \PHPUnit\DbUnit\DataSet\CompositeDataSet();
$fixturePaths = [
"test/Seeds/Upc/DB/UpcSelect.xml",
"test/Seeds/Generic/DB/ProductUpcSelect.xml"
];
foreach($fixturePaths as $fixturePath) {
$dataSet = $this->createXmlDataSet($fixturePath);
$primary->addDataSet($dataSet);
}
return $primary;
}
Then after my query I'm attempting to call this user-defined function...
protected function compareDatabase(String $seedPath, String $table) {
$expected = $this->createFlatXmlDataSet($seedPath)->getTable($table);
$result = $this->getConnection()->createQueryTable($table, "SELECT * FROM $table");
$this->assertTablesEqual($expected, $result);
}
The idea here is that I have an initial DB state, run my query, then compare the actual table state with the XML data set representing what I expect the table to look like. This process is described in PHPUnit's documentation for DBUnit but I keep having an exception thrown...
PHPUnit\DbUnit\InvalidArgumentException: There is already a table named upc with different table definition
Test example...
public function testDeleteByUpc() {
$mapper = new UpcMapper($this->getPdo());
$mapper->deleteByUpc("someUpcCode1");
$this->compareDatabase("test/Seeds/Upc/DB/UpcAfterDelete.xml", 'upc');
}
I seem to be following the docs...how is this supposed to be done?
This was actually unrelated to creating a second XML Dataset. This exception was thrown because the two fixtures I loaded in my getDataSet() method both had table definitions for upc.
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. :)
I am attempting to test a simple Laravel model which has required "password" and "email" properties. My test reads as follows…
public function testEmailIsRequired() {
$user = new User;
$user->password = 'derp';
// should not save
$this->assertFalse($user->save());
}
Rather than correctly agree that "this doesn't work" and a successful test, I’m getting…
1) UserTest::testEmailIsRequired
Illuminate\Database\QueryException: SQLSTATE[23000]: Integrity constraint violation:
19 NOT NULL constraint failed: users.email
(SQL: insert into "users" ("password", "updated_at", "created_at")
values (derp, 2014-09-26 15:27:07, 2014-09-26 15:27:07))
[...]
FAILURES!
Tests: 1, Assertions: 0, Errors: 1.
I’m afraid that I’m a total novice here. It seems to me that PHPUnit ought to be suppressing the database driver’s own error reporting and just accepting "false"… help?
Your test is looking for the function to return false. Any other errors generated in your function will still be returned, since you are calling the function.
public function testEmailIsRequired()
{
$user = new User;
$user->password = 'derp';
// should not save
$this->assertFalse($user->save());
}
public function testMockSaveWorks()
{
$Mock = $this->getMock('User', array('save'));
// Set up the expectation for the getResult() method
$Mock->expects($this->any())
->method('save')
->will($this->returnValue(true));
$this->assertTrue($Mock->save());
}
class User()
{
...
public function save()
{
if( is_null($this->email) )
return false;
...
return true;
}
}
You should then use Mocks to remove the actual save() function and its tie in to your database, and have the mock return true to pretend to save the record to show that you have tested the module.
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