I have two entities. First called "Status":
<?php
class Status {
protected $id;
protected $type = null; // standarized type of status (f.e. "locked", "disabled")
protected $value = true; // true or false (value of status)
protected $change_reason = null;
protected $changed_by;
protected $changed_at;
}
I've cleared annotations for better readability.
And the second called eg. Account. Because Account is not the only one entity using Statuses, relations beetwen Status and any other "statusable" entity (I think) should be many-to-many. For Account there would be join table account_status etc.
Additionaly one status belongs to only one Entity.
It all works in that configuration, but I really don't know how to retrieve a list of accounts with their latest statuses.
I wrote an SQL query to retrieve actual statuses:
SELECT * FROM (
SELECT t.type, t.value
FROM status AS t
ORDER BY t.changed_at DESC
) AS t1 GROUP BY t1.type
My questions are:
Is that idea correct at all?
How to retrieve a list of accounts with all their latest statuses?
Sorry for my poor English.
EDIT:
I want just to get an account, join its latest statuses, and then get them simply by: $task -> getStatus('highlighted') to get a value of a latest (youngest) status of type "highlighted"
EDIT2:
ideal would be still have ability to sort and filter by status of given type
class Task {
// this is list of all statuses
protected $statusHistory;
protected $lastStatus;
public function __construct() {
$this->statusHistory = new Arraycollection();
}
public function addStatus($status) {
$this->statusHistory[] = $status;
$this->lastStatus = $status;
}
public function getStatusHistory() {
return $this->statusHistory;
}
public function getLastStatus() {
return $this->lastStatus;
}
}
// to get list of statuses
$task->getStatusHistory();
// to get last status, it returns Status object, not collection
$task->getLastStatus();
It's more or less standard approach when you need first/last element from collection and getting the whole collection might be an overhead.
Related
I wish to create add an action that goes to the new action but pre-populates the entity based on added url parameters. For example to set an association.
The documentation shows how to override the createEntity method the set values, but this method has no way to get the parameters from the request so I cannot set the association based on a passed parameter.
This is similar to How to set a default value in AssociationField EasyAdmin 3 but as mentioned in this case the request is not available to use.
Turns out we can get the request from the request stack.
public function createEntity(string $entityFqcn)
{
/** #var AgentAccreditation $entity */
$entity = parent::createEntity($entityFqcn);
$request = $this->get('request_stack')->getCurrentRequest();
if ($agentId = $request->query->get('agentId')) {
$agentRepository = $this->getDoctrine()->getRepository(Agent::class);
$agent = $agentRepository->find($agentId);
$entity->setAgent($agent);
}
return $entity;
}
}
I have two entities.
First is my Parent entity, that has property called productsCount.
This entity has some another entity linked to it called Store and store have linked Room. Each room can have multiple products in it.
When I edit products that are assigned to room, I want to update the Parent productsCount to store count of all products in all rooms in all stores.
I have a SQL query that does calculate the count. I need to do this each time I update the Room with new products. This is done using preFlush hook using EntityListener on the Room entity.
The preFlush hook gets triggered properly, but then it will timeout for some reason.
Here is the preFlush code example
public function preFlush(Room $room, PreFlushEventArgs $args)
{
$em = $args->getEntityManager();
$parent = $room->getStore()->getParent();
$rsm = new ResultSetMapping();
$rsm->addScalarResult('COUNT(product_id)', 'count');
$query = $em->createNativeQuery(
'select COUNT(product_id)
from room_product where `room_id` in
(select id from room where store_id in
(select id from store where parent_id = :parentId))', $rsm);
$query->setParameter('parentId', $parent->getId());
$result = $query->getOneOrNullResult();
$parent->setNumberOfServices($result['count']);
$em->persist($parent);
$em->flush();
}
The query should be working fine, so I think it has something to do with flushing and persisting the parent entity.
Any ideas?
So, I was able to find a solution after few hours of fighting.
Turns out I dont have to persist, nor flush the changes. The solution is to use Unit of Work to recompute changes on the $parent and then doctrine will flush them by its own. I also had to change the way I count the products, as in the preFlush stage the change is not yet in the database, so the query will not work properly. Thats why I count them manually by traversing the tree of relations.
Here is a code sample.
public function preFlush(Room $room, PreFlushEventArgs $args)
{
$em = $args->getEntityManager();
$numOfProducts = 0;
$parent = $room->getStore()->getParent();
foreach($parent->getStores() as $store) {
foreach($store->getRooms() as $room) {
$numOfServices += count($room->getProducts());
}
}
$parent->setNumberOfProducts($numOfProducts);
$classMetadata = $em->getClassMetadata(get_class($parent));
$em->getUnitOfWork()->computeChangeSet($classMetadata, $parent);
}
The reason is recursion: you're calling $em->flush(); from the subscriber, EntityManager enters flush, fires preFlush event which calls your handler which again calls $em->flush() and so on.
IIRC preFlush is called before change set calculation so just updating your entity with new value should be enough for Doctrine to detect said change.
Say you have a Category entity :
class Category
{
/**
* #Gedmo\Translatable
*/
private $categoryName;
}
And a Module entity.
Each Module belongs to one Category :
class Module
{
/**
* #Gedmo\Translatable
*/
private $moduleName;
/**
* #ManyToOne(targetEntity="Module")
*/
private $category;
}
Using the query hint I can get a Module translated to a specific locale :
function getModule($id, $locale)
{
$query = $em->createQuery('select m from AppBundle\Entity\Module m where m.id=' . $id);
$query->setHint(TranslatableListener::HINT_TRANSLATABLE_LOCALE, $locale);
$query->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
$module = $query->getSingleResult();
return $module;
}
Testing :
$module = getModule(1234, 'en');
// SUCCEED
$this->assertEquals('module name in english', $module->getModuleName();
$category = $module->getCategory();
// FAIL !
$this->assertEquals('category name in english', $category->getModuleName();
// SUCCEED (but one more request)
$category->setTranslatableLocale('en');
$em->refresh($category);
$this->assertEquals('category name in english', $category->getModuleName();
In few words : the query hint is able to insert a JOIN to the SELECT statement for the Module entity.
Call to $module->getCategory() will trigger lazy loading. But this time, there will be no translation JOINed.
Meaning I have to explicitly ask for the english version of the category.
Question : is it possible to have associated entities translated when querying the owner entity ? (having the FAILed test to succeed)
I already tried eager fetching on the ManyToOne association. Won't work because I am querying through DQL.
UPDATE :
To make this specific example to work (and have a better code) I can JOIN the Category in my DQL :
select m,c from Module join m.category where m.id=id
This will load the module and its category with the correct translations.
But in a perfect world I would love to have a generic code like this one :
protected function getEntityById($className, $id, $locale = null)
{
$dql = 'select e from AppBundle\Entity\\' . $className . ' e where e.id=' . $id;
$query = $this->em->createQuery($dql);
$query->setHint(
\Gedmo\Translatable\TranslatableListener::HINT_TRANSLATABLE_LOCALE,
$locale
);
$query->setHint(
\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER,
'Gedmo\\Translatable\\Query\\TreeWalker\\TranslationWalker'
);
$entity = $query->getSingleResult();
return $entity;
}
How to update this piece of code to have the associated entities to be loaded with the correct locale ?
I'm afraid there is no way to affect the behaviour of $module->getCategory(). Association's loading can only be affected by using doctrine filters. Unfortunately these filters can only add to WHERE part of request, so it is not possible to add join.
I'd create a function in category repository to get category with the proper translation like you did in the getModule(). I don't think there is another way. Ex:
$category = $categoryRepository->findByModule18n($module, $locale);
I am having an issue with EF 4.1 using "Code First". Let me setup my situation before I start posting any code. I have my DBContext class, called MemberSalesContext, in a class library project called Data.EF. I have my POCOs in a seperate class library project called Domain. My Domain project knows nothing of Entity Framework, no references, no nothing. My Data.EF project has a reference to the Domain project so that my DB context class can wire up everything in my mapping classes located in Data.EF.Mapping. I am doing all of the mappings in this namespace using the EntityTypeConfiguration class from EntityFramework. All of this is pretty standard stuff. On top of Entity Framework, I am using the Repository pattern and the Specification pattern.
My SQL Server database table has a composite primary key defined. The three columns that are part of the key are Batch_ID, RecDate, and Supplier_Date. This table as an identity column (database generated value => +1) called XREF_ID, which is not part of the PK.
My mapping class, located in Data.EF.Mapping looks like the following:
public class CrossReferenceMapping : EntityTypeConfiguration<CrossReference>
{
public CrossReferenceMapping()
{
HasKey(cpk => cpk.Batch_ID);
HasKey(cpk => cpk.RecDate);
HasKey(cpk => cpk.Supplier_Date);
Property(p => p.XREF_ID).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
ToTable("wPRSBatchXREF");
}
}
My MemberSalesContext class (inherits from DBContext) looks like the following:
public class MemberSalesContext : DbContext, IDbContext
{
//...more DbSets here...
public DbSet<CrossReference> CrossReferences { get; set; }
//...more DbSets here...
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Remove<IncludeMetadataConvention>();
//...more modelBuilder here...
modelBuilder.Configurations.Add<CrossReference>(new CrossReferenceMapping());
//...more modelBuilder here...
}
}
I have a private method in a class that uses my repository to return a list of objects that get iterated over. The list I am referring to is the outermost foreach loop in the example below.
private void CloseAllReports()
{
//* get list of completed reports and close each one (populate batches)
foreach (SalesReport salesReport in GetCompletedSalesReports())
{
try
{
//* aggregate sales and revenue by each distinct supplier_date in this report
var aggregates = BatchSalesRevenue(salesReport);
//* ensure that the entire SalesReport breaks out into Batches; success or failure per SalesReport
_repository.UnitOfWork.BeginTransaction();
//* each salesReport here will result in one-to-many batches
foreach (AggregateBySupplierDate aggregate in aggregates)
{
//* get the batch range (type) from the repository
BatchType batchType = _repository.Single<BatchType>(new BatchTypeSpecification(salesReport.Batch_Type));
//* get xref from repository, *if available*
//* some will have already populated the XREF
CrossReference crossReference = _repository.Single<CrossReference>(new CrossReferenceSpecification(salesReport.Batch_ID, salesReport.RecDate, aggregate.SupplierDate));
//* create a new batch
PRSBatch batch = new PRSBatch(salesReport,
aggregate.SupplierDate,
BatchTypeCode(batchType.Description),
BatchControlNumber(batchType.Description, salesReport.RecDate, BatchTypeCode(batchType.Description)),
salesReport.Zero_Sales_Flag == false ? aggregate.SalesAmount : 1,
salesReport.Zero_Sales_Flag == false ? aggregate.RevenueAmount : 0);
//* populate CrossReference property; this will either be a crossReference object, or null
batch.CrossReference = crossReference;
//* close the batch
//* see PRSBatch partial class for business rule implementations
batch.Close();
//* check XREF to see if it needs to be added to the repository
if (crossReference == null)
{
//*add the Xref to the repository
_repository.Add<CrossReference>(batch.CrossReference);
}
//* add batch to the repository
_repository.Add<PRSBatch>(batch);
}
_repository.UnitOfWork.CommitTransaction();
}
catch (Exception ex)
{
//* log the error
_logger.Log(User, ex.Message.ToString().Trim(), ex.Source.ToString().Trim(), ex.StackTrace.ToString().Trim());
//* move on to the next completed salesReport
}
}
}
All goes well on the first iteration of the outer loop. On the second iteration of the outer loop, the code fails at _repository.UnitOfWork.CommitTransaction(). The error message returned is the following:
"The changes to the database were committed successfully, but an error occurred while updating the object context. The ObjectContext might be in an inconsistent state. Inner exception message: AcceptChanges cannot continue because the object's key values conflict with another object in the ObjectStateManager. Make sure that the key values are unique before calling AcceptChanges."
In this situation, the database changes on the second iteration were not committed successfully, but the changes in the first iteration were. I have ensured that objects in the outer and inner loops are all unique, adhering to the database primary keys.
Is there something that I am missing here? I am willing to augment my code samples, if it proves helpful. I have done everything within my capabilities to troubleshoot this issue, minus modifying the composite primary key set on the database table.
Can anyone help??? Much thanks in advance! BTW, sorry for the long post!
I am answering my own question here...
My issue had to do with how the composite primary key was being defined in my mapping class. When defining a composite primary key using EF Code First, you must define it like so:
HasKey(cpk => new { cpk.COMPANYID, cpk.RecDate, cpk.BATTYPCD, cpk.BATCTLNO });
As opposed to how I had it defined previously:
HasKey(cpk => cpk.COMPANYID);
HasKey(cpk => cpk.RecDate);
HasKey(cpk => cpk.BATTYPCD);
HasKey(cpk => cpk.BATCTLNO);
The error I was receiving was that the ObjectContext contained multiple elements of the same type that were not unique. This became an issue in my UnitOfWork on CommitTransaction. This is because when the mapping class was instanciated from my DBContext class, it executed 4 HasKey statements shown above, with only the last one for property BATCTLNO becoming the primary key (not composite). Defining them inline, as in my first code sample above, resolves the issue.
Hope this helps someone!
I've set up a self-referencing entity per the manual here:
http://www.google.com/url?sa=D&q=http://www.doctrine-project.org/docs/orm/2.0/en/reference/association-mapping.html%23one-to-many-self-referencing
My class is Page (instead of Category, like in the docs). In my entity
class I have a toArray() method that I've implemented that will give
me back the values of my member variables. For those fields that are
associations, I've made sure to grab the associated class object then
grab the id. I'm doing this to populate a form. Here is the code from
my toArray() method in my Page entity as well as my PageService
function to grab a Page object and my Page Controller code that calls
toArray() to populate my form.
http://pastie.org/1686419
As I say in the code comments, when the toArray() method is called in
the Page Controller, all values get populated except for parent id.
page_type is also a ManyToOne association and it gets populated no
problem. Explicitly grabbing the parent id from the Page object
outside of the toArray() method (in the Page Controller) does return
the parent id value. (See code.)
As a side note, I'm using __get() and __set() in my Page entity instead of full blown getters/setters.
I think it is because you are getting caught out by proxies. When you have an association in Doctrine 2, the related objects are not returned directly as objects, but as subclasses which do not fill their properties until a method is called (because of lazy loading to save database queries).
Since you are calling the property directly (with $this->parent->id) without invoking any method the object properties are all empty.
This page http://www.doctrine-project.org/docs/orm/2.0/en/tutorials/getting-started-xml-edition.html#a-first-prototype has a warning about this type of thing in the warning box. Although yours isn't a public property, you are accessing as though it were because that object is of the same class and the same problem is occuring.
Not sure of exactly what is causing your described behavior, but you're probably better anyway to have your toArray() method call getters/setters rather than having toArray() operate directly on the class properties. This will give you consistency so that if you implement custom getters for certain properties, you'll always get back the same result from toArray() and the getter.
A rough example:
<?php
/** #Entity */
class MyEntity {
// ....
/** #Column */
protected $foo;
public function setFoo($val)
{
$this->foo = $val;
}
public function getFoo()
{
return 'hello ' . $this->foo;
}
public function toArray()
{
$fields = array('foo');
$values = array();
foreach($fields as $field) {
$method = 'get' . ucfirst($field);
if (is_callable(array($this, $method)) {
$fields[$field] = $this->$method();
} else {
$fields[$field] = $this->$field;
}
}
return $fields;
}
}
Now you get the same result:
<?php
$e = new MyEntity;
$e->setFoo('world');
$e->getFoo(); // returns 'hello world'
$e->toArray(); // returns array('foo' => 'hello world')