Doctrine OneToMany with aggregate field, how to keep it up to date? - collections

In doctrine2 I have a OneToMany association: One Application <=> Many ApplicationCost
// Application.php
/**
* #ORM\OneToMany(targetEntity="ApplicationCost", mappedBy="application", orphanRemoval=true)
*/
protected $costs;
// ApplicationCost.php
/**
* #ORM\ManyToOne(targetEntity="Application", inversedBy="costs")
* #ORM\JoinColumn(name="application_id", referencedColumnName="id")
*/
protected $application;
In Application entity I have an agregate field sumCosts:
/**
* #ORM\Column(type="decimal", scale=2)
*/
protected $sumCosts;
Which is updated when addCost and removeCost are called:
// Application.php
public function addCost(ApplicationCost $cost)
{
if (!$this->costs->contains($cost)) {
$this->sumCosts += $cost->getBalance();
$this->costs[] = $cost;
$cost->setApplication($this);
}
return $this;
}
public function removeCost(ApplicationCost $cost)
{
if ($this->costs->contains($cost)) {
$this->sumCosts -= $cost->getBalance();
$this->costs->removeElement($cost);
}
}
Assuming User can edit already existing ApplicationCost's and can change it's parent Application, how do I make sure that this agregate field is up to date?
My approach is:
// ApplicationCost.php
public function setApplication(Application $application = null)
{
if ($this->application !== null) {
$this->application->removeCost($this);
}
if ($application !== null) {
$application->addCost($this);
}
$this->application = $application;
return $this;
}
Is that good? Or am I makeing here some huge mistake here and sumCosts may be out of sync?
EDIT: I've read Doctrine's Aggregate Fields cookbook and I have the versioning (and I use locking mechanism). My question is not about concurrency.
EDIT: I've created some tests
public function testSumCosts()
{
$app = new Application();
$costA = new ApplicationCost();
$costA->setBalance(150);
$costB = new ApplicationCost();
$costB->setBalance(100);
$costC = new ApplicationCost();
$costC->setBalance(50);
$app->addCost($costA);
$app->addCost($costB);
$app->addCost($costC);
$app->removeCost($costC);
$this->assertEquals(250, $app->sumCosts(), 'Costs are summed correctly');
}
public function testCostsChangeApplication()
{
$appA = new Application();
$appB = new Application();
$costA = new ApplicationCost();
$costA->setBalance(100);
$costB = new ApplicationCost();
$costB->setBalance(50);
$appA->addCost($costA);
$appB->addCost($costB);
$costA->setApplication($appB);
$costB->setApplication(null);
$this->assertEquals(0, $appA->sumCosts(), 'Costs are removed correctly');
$this->assertEquals(100, $appB->sumCosts(), 'Costs are added correctly');
}
And after adding $cost->setApplication($this); to addEntry both tests are green. Though I still wonder if I might have missed something.

Okay, I think I finally achieved desired result. I'll describe it for future reference and anyone who might have the same problem:
First of all correct the class
// Application.php
public function addCost(ApplicationCost $cost)
{
if (!$this->costs->contains($cost)) {
$this->sumCosts += $cost->getBalance();
}
$this->costs[] = $cost;
return $this;
}
public function removeCost(ApplicationCost $cost)
{
if ($this->costs->contains($cost)) {
$this->sumCosts -= $cost->getBalance();
}
$this->costs->removeElement($cost);
}
If you compare this to my original code you'll see that only updateing the agregate field is under condition. It does not hurt as collections can't hold duplicate elements and can't remove non existing elements.
Second of all, configure the cascade={all} option on inverse side of association (that is, on costs inside Application.php). So whenever you add/remove costs they are persisted too.
to be continued... (have to test what happens when i change application it from the owning side and persist only ApplicationCost -> will both old and new Application be updated?)

Related

Sorting list of VirtualPages on a field from its Page

I have an AreaPage with $many_many VirtualPages:
class AreaPage extends Page {
/**
* #var array
*/
private static $many_many = [
'RelatedVirtualPages' => 'VirtualPage'
];
// ...
}
The RelatedVirtualPages are copying content from ContentPages:
class ContentPage extends Page {
/**
* #var array
*/
private static $db = [
'Highlighted' => 'Boolean'
];
// ...
}
What's the best way to sort RelatedVirtualPages on the Highlighted db field of the ContentPage that it's copying?
Virtual Pages could be pointed at pages of different types and there is no enforcement that all of those pages are ContentPages, or at least pages that have a Hightlighted db field. You can ensure this manually when you create your SiteTree, but users could come along and screw it up so keep this in mind.
Here is some psuedo-code that might help you get started. It assumes that all virtual pages are ContentPages. If you will have multiple types of VirtualPages referenced by an AreaPage then this is probably not sufficient.
$virtualPages = $myAreaPage->RelatedVirtualPages();
$contentSourcePages = ContentPage::get()->byIDs($virtualPage->column('CopyContentFromID'));
$sortedSourcePages = $contentSourcePages->sort('Highlighted','ASC');
You possibly could also use an innerJoin, but then you have to deal with _Live tables and possibly multiple page tables (again if not just using ContentPage as VirtualPage) which could lead to some complicated scenarios.
Update
So, to summarize in my own words, you need a list of the VirtualContentPages linked to a specific AreaPage sorted on the Highlighted field from the ContentPage that each VirtualContentPage links to. If this summary is accurate, would this work:
$sortedVirtualPages = $myAreaPage->RelatedVirtualPages()
->innerJoin('ContentPage', '"ContentPage"."ID" = "VirtualContentPage"."CopyContentFromID"')
->sort('Highlighted DESC');
I could not find a very clean method, but did find two ways to achieve this. The function goes in the class AreaPage
First
public function getRelatedVirtualPages()
{
$items = $this->getManyManyComponents('RelatedVirtualPages');
$highlighted = $items->filterByCallback(function($record, $list) {
if($record->CopyContentFrom() instanceOf ContentPage) {
//return ! $record->CopyContentFrom()->Highlighted; // ASC
return $record->CopyContentFrom()->Highlighted; // DESC
}
});
$highlighted->merge($items);
$highlighted->removeDuplicates();
return $highlighted;
}
Second (the method you described in the comments)
public function getRelatedVirtualPages()
{
$items = $this->getManyManyComponents('RelatedVirtualPages');
$arrayList = new ArrayList();
foreach($items as $virtualPage)
{
if($virtualPage->CopyContentFrom() instanceOf ContentPage) {
$virtualPage->Highlighted = $virtualPage->CopyContentFrom()->Highlighted;
$arrayList->push($virtualPage);
}
}
$arrayList = $arrayList->sort('Highlighted DESC');
return $arrayList;
}
I'm not very proud of any of these solutions, but I believe they do fit your criteria.
Here's what I ended up doing, which I think works:
/**
* #return ArrayList
*/
public function VirtualPages()
{
$result = [];
$virtualPages = $this->RelatedVirtualPages();
$contentPages = ContentPage::get()
->byIDs($virtualPages->column('CopyContentFromID'))
->map('ID', 'Highlighted')
->toArray();
foreach($virtualPages as $virtualPage) {
$highlighted = $contentPages[$virtualPage->CopyContentFromID];
$virtualPage->Highlighted = $highlighted;
$result[] = $virtualPage;
}
return ArrayList::create(
$result
);
}
And then it's sortable like so:
$areaPage->VirtualPages()->sort('Highlighted DESC');
Thank you for all the answers and pointers. I'll wait a bit before marking any answer.
Couldn't you just do
//just get one areapage
$AreaPageItem = AreaPage::get()->First();
//now get the RelatedVirtualPages sorted
$related_pages = $AreaPageItem->RelatedVirtualPages()->sort("Highlighted","ASC");

Get new value of entity field after Doctrine flush

I'm trying to resize an image after persisting an entity with Doctrine. In my Entity code, I'm setting a field to a specific value before the flush and the update :
/**
* #ORM\PrePersist()
* #ORM\PreUpdate()
*/
public function preUpload()
{
if (null !== $this->getFile()) {
// do whatever you want to generate a unique name
$filename = sha1(uniqid(mt_rand(), true));
$this->image = $filename.'.png';
}
}
So the image field is supposed to be updated.
Then in my controller, I'd like to do my resize job:
if ($form->isValid())
{
$em->persist($activite);
$em->flush();
//resize the image
$img_path = $activite->getImage();
resizeImage($img_path);
}
However, at this point in the code, the value of $activite->image is still null. How can I get the new value?
(Everything is saved well in the database.)
The EntityManager has a refresh() method to update your entity with the latest values from database.
$em->refresh($entity);
I found my error.
Actually, I was following this tutorial: http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html
and at some point they give this code to set the file:
public function setFile(UploadedFile $file = null)
{
$this->file = $file;
// check if we have an old image path
if (isset($this->path)) {
// store the old name to delete after the update
$this->temp = $this->path;
$this->path = null;
} else {
$this->path = 'initial';
}
}
And then after the upload, in the first version (with the random filename), they do :
$this->file = null;
But then in the second version, this code is replace by:
$this->setFile(null);
My problem is that I've tried the two versions to finally come back to the first. However, I forgot to change the line to set the file to null and so everytime my path field was reset to null.
Sorry for this absurdity and thanks for your help.

Symfony2 Nested forms and ManyToMany relations

I'm trying to handle a form which is quite complex for me...
We have Collections, which contain books (OneToMany), with articles(OneToMany) and their authors (ManyToMany).
The user can edit a book: he can add or remove an article, and add or remove some authors for each article. There are nested forms : book>article>author.
If the author is new in the Collection, it is created for that Collection.
Entities descriptions look fine, database is generated by the console and seems consistent.
This is working fine if I don't have to deal with authors using the book edition form. If the author exists, I have a duplicate entry bug. If the author is new, I have a "Explicitly persist the new entity or configure cascading persist operations on the relationship" bug.
Here is the code:
public function onSuccess(Book $book)
{
$this->em->persist($book);
foreach($book->getArticles() as $article)
{
$article->setUrlname($this->mu->generateUrlname($article->getName()));
$article->setBook($book);
// Saving (and creating) the authors of the book
foreach ($this->collectionWithAuthors->getAuthors() as $existAuthor){
foreach($article->getAuthors() as $author) {
$authorUrlname=$this->mu->generateUrlname($author->getFirstname().' '.$author->getLastname());
if ( $existAuthor->getUrlname() == $authorUrlname) { // The author is existing
$article->addAuthor($existAuthor);
$this->em->persist($existAuthor);
}else{ // New Author
$newAuthor = new Author();
$newAuthor->setCollection($this->collectionWithBaseArticles);
$newAuthor->setLastname($author->getLastname());
$newAuthor->setFirstname($author->getFirstname());
$newAuthor->setUrlname($authorUrlname);
$this->em->persist($newAuthor);
$article->addAuthor($newAuthor);
}
}
}
$this->em->persist($article);
}
$this->em->flush();
}
I don't know how to use cascades. But the $article->addAuthor() is supposed to call $authors->addArticle():
Article Entity extract
/**
* #ORM\ManyToMany(targetEntity="bnd\myBundle\Entity\Author", mappedBy="articles")
*/
private $authors;
/**
* Add authors
*
* #param bnd\myBundle\Entity\Author $authors
* #return Article
*/
public function addAuthor(\bnd\myBundle\Entity\Author $authors)
{
$this->authors[] = $authors;
$authors->addArticle($this);
}
The logic in foreach statement is wrong. Suppose we have next authors:
Persisted authors (collectionWithAuthors):
John
Eric
Submitted authors
Ada
Eric
So for every existing author (John and Eric) the script loop thru new authors:
foreach ([John, Eric] as $author) {
foreach([Ada, Eric] as $newAuthor) {
// John author: the script persist Ada(right) and Eric(wrong) as new authors
// Eric author: the script persist Ada(wrong), but not Eric(right)
}
}
The solution is to replace article authors with existing authors (if there is similar)
foreach ($article->getAuthors() as $key => $articleAuthor) {
$authorUrlname=$this->mu->generateUrlname($articleAuthor->getFirstname().' '.$articleAuthor->getLastname());
$foundAuthor = false;
// Compare article author with each existing author
foreach ($this->collectionWithAuthors->getAuthors() as $existAuthor) {
if ($existAuthor->getUrlname() == $authorUrlname) {
$foundAuthor = true;
break; // It has found similar author no need to look further
}
}
// Use $existAuthor as found one, otherwise use $articleAuthor
if ($foundAuthor) {
$article->removeAuthor($articleAuthor); // Remove submitted author, so he wont be persisted to database
$article->addAuthor($existAuthor);
} else {
// Here you dont need to create new author
$articleAuthor->setCollection($this->collectionWithBaseArticles);
$articleAuthor->setUrlname($authorUrlname);
}
...
}
$this->_em->persist($article);
You have noticed i removed any author persistent from the loop, to persist these authors its better to set cascade={'persist'} in $authors annotation of Article Entity Class
/**
* #ORM\ManyToMany(targetEntity="Author", cascade={"persist"})
* #ORM\JoinTable(...)
*/
protected $authors;
UPD:
I forgot to mention one thing about cascade persistent. To persist relation betweed article and author you also have to add relation to author entity. Edit the addAuthor() method in the Article entity as below:
public function addAuthor(Author $author)
{
// Only add author relation if the article does not have it already
if (!$this->authors->contains($author)) {
$this->authors[] = $author;
$author->addArticle($this);
}
}
Also, it's a good practice to define default values for a collection of entities in the constructor:
use Doctrine\Common\Collections\ArrayCollection;
// ...
public function __construct()
{
$this->authors = new ArrayCollection();
}

Help me write a PHPUnit Test for the following method

public function getAvailableVideosByRfid($rfid, $count=200) {
$query="SELECT id FROM sometable WHERE rfid='$rfid'";
$result = mysql_query($query);
$count2 = mysql_num_rows($result);
if ($count2){ //this rfid has been claimed
return 0;
}
My assertions are :
1). $rfid is a string 5 characters long
2). I am getting a valid result set
Thank You
Please assume that I have the following Unit Test code:
class videoSharingTest extends PHPUnit_Framework_TestCase {
/**
* #var videoSharing
*/
protected $object;
/**
* Sets up the fixture, for example, opens a network connection.
* This method is called before a test is executed.
*/
protected function setUp() {
$this->object = new videoSharing;
}
/**
* Tears down the fixture, for example, closes a network connection.
* This method is called after a test is executed.
*/
protected function tearDown() {
}
public function testGetAllVideosByRfid() {
******What should I put here*****
}
You need to decentralize your database, typically with a Database abstraction layer which you would mock out. Thus adding a ->setDatabase(), etc. on the object that has the method you are using. Then inside your setUp() { ... } you would set the Database object to a mock:
$this->object->setDatabase($mockDb);
Then you would change
$result = mysql_query($query);
$count2 = mysql_num_rows($result);
to use some form of PDO - so that you could call setDatabase() with a PDO Sql Lite. For example:
setUp() { $this->object->setDatabase($mockDb); }
testFunction() {
$rfid = 'the rfid to use in the test';
//make sure no videos exist yet
$this->assertEquals(0, count($this->object->getAvailableVideosByRfid($rfid, ..);
//you may want to assert that it returns a null/false/empty array/etc.
$db = $this->object->getDatabase();
$records = array(... some data ...);
$db->insert($records); //psuedo code
$vids = $this->object->getAvailableVideosByRfid($rfid, ..); //however you map
$this->assertEquals(count($records), count(vids));
foreach($vids as $video) {
//here you would map the $video to the corresponidng $record to make sure all
vital data was stored and retrieved from the method.
}
}
Typically this would all be done in PDO Sqlite so that no true database would be made/created just for the unit test & that it would live and die with the test, and any developer anywhere could use it with no configuration needed.

Cookies across multiple WAR files

I'm creating a facelets template for all my company's internal applications. Its appearance is based on the skin which the user selects (like gmail themes).
It makes sense to store the user's preferred skin in a cookie.
My "user-preferences" WAR can see this cookie. However, my other applications are unable to find the cookie. They are on the same domain/subdomain as the user-preferences WAR.
Is there some reason for this?
Here is my bean which is used to create/find the preferred skin. This same file is used in all the projects:
// BackingBeanBase is just a class with convenience methods. Doesn't
// really affect anything here.
public class UserSkinBean extends BackingBeanBase {
private final static String SKIN_COOKIE_NAME = "preferredSkin";
private final static String DEFAULT_SKIN_NAME = "classic";
/**
* Get the name of the user's preferred skin. If this value wasn't set previously,
* it will return a default value.
*
* #return
*/
public String getSkinName() {
Cookie skinNameCookie = findSkinCookie();
if (skinNameCookie == null) {
skinNameCookie = initializeSkinNameCookie(DEFAULT_SKIN_NAME);
addCookie(skinNameCookie);
}
return skinNameCookie.getValue();
}
/**
* Set the skin to the given name. Must be the name of a valid richFaces skin.
*
* #param skinName
*/
public void setSkinName(String skinName) {
if (skinName == null) {
skinName = DEFAULT_SKIN_NAME;
}
Cookie skinNameCookie = findSkinCookie();
if (skinNameCookie == null) {
skinNameCookie = initializeSkinNameCookie(skinName);
}
else {
skinNameCookie.setValue(skinName);
}
addCookie(skinNameCookie);
}
private void addCookie(Cookie skinNameCookie) {
((HttpServletResponse)getFacesContext().getExternalContext().getResponse()).addCookie(skinNameCookie);
}
private Cookie initializeSkinNameCookie(String skinName) {
Cookie ret = new Cookie(SKIN_COOKIE_NAME, skinName);
ret.setComment("The purpose of this cookie is to hold the name of the user's preferred richFaces skin.");
//set the max age to one year.
ret.setMaxAge(60 * 60 * 24 * 365);
ret.setPath("/");
return ret;
}
private Cookie findSkinCookie() {
Cookie[] cookies = ((HttpServletRequest)getFacesContext().getExternalContext().getRequest()).getCookies();
Cookie ret = null;
for (Cookie cookie : cookies) {
if (cookie.getName().equals(SKIN_COOKIE_NAME)) {
ret = cookie;
break;
}
}
return ret;
}
}
Can anyone see what I'm doing wrong?
Update: I've narrowed it down a bit...it works fine in FF, but IE still doesn't like it (of course).
Thanks,
Zack
I think you need to assign domain/subdomain to the cookies.
Like, (Note that the domain should start with a dot)
ret.setDomain(".test.com");
ret.setDomain(".test.co.uk");
http://www.apl.jhu.edu/~hall/java/Servlet-Tutorial/Servlet-Tutorial-Cookies.html
I found a solution.
I just used javascript on the client-side to create the cookies.
This worked fine.

Resources