Commit 5f0b0cc4 authored by Karamel's avatar Karamel
Browse files

Fix fromStruct and Doctrine for new embedded objects. Routes for floors and places.

parent a5bf2252
<?php
// Nothing here right now
use \Pasteque\Server\Model\Floor;
use \Pasteque\Server\System\API\APICaller;
use \Pasteque\Server\System\API\APIResult;
$app->POST('/api/places', function($request, $response, $args) {
$ptApp = $this->get('settings')['ptApp'];
$structFloors = $request->getParsedBody();
if ($structFloors === null) {
return $response->withStatus(400, 'Unable to parse input data');
}
$floors = [];
foreach ($structFloors as $jsFloor) {
$floor = Floor::fromStruct($jsFloor, $ptApp->getDao());
$floors[] = $floor;
}
return $response->withApiResult(APICaller::run($ptApp, 'place', 'write', [$floors]));
});
......@@ -47,6 +47,53 @@ class SubclassHelper
$this->invertedField = $invertedField;
}
/** Order the DAO to delete all subinstances from records from database
* that are not linked in $mainrecords.
* It doesn't commit changes nor clear the subclass collection
* from the main records. */
public function cleanSubrecords($mainrecords) {
// Delete subclasses instances, usefull before deleting the main class
/* Required because the owning side in Doctrine is on subclass
* thus cascade deletes main record when deleting them and not
* the contrary */
$mainrecords = (is_array($mainrecords)) ? $mainrecords : array($mainrecords);
$subrecs = array();
$getName = 'get' . ucfirst($this->subclassField);
foreach ($mainrecords as $rec) {
if ($rec->getId() === null) {
// New record, nothing special to do
continue;
}
// Get subrecords from database
$subrecords = $this->dao->search($this->subclassName,
new DAOCondition($this->invertedField, '=', $rec));
// Look for it in $rec, if not found add it to delete candidates
try {
foreach ($subrecords as $subrecord) {
$reflectionGet = new \ReflectionMethod($rec, $getName);
$mainSubs = $reflectionGet->invoke($rec);
$found = false;
foreach ($mainSubs as $mainSub) {
if ($mainSub->getId() === $subrecord->getId()) {
$found = true;
break;
}
}
if (!$found) {
$subrecs[] = $subrecord;
}
}
} catch (\ReflectionException $e) {
throw new \InvalidArgumentException(sprintf('%s does not implements %s',
get_class($rec), $getName));
}
}
// Delete candidates
foreach ($subrecs as $record) {
$this->dao->delete($record);
}
}
/** Order the DAO to delete all subinstances from records from database.
* It doesn't read subrecords from $mainrecords but will
* fetch for existing data in database and delete them.
......
......@@ -55,8 +55,12 @@ class CashSessionCat extends DoctrineModel // Embedded class
}
public function getId() {
return array('cashSession' => $this->getCashSession()->getId(),
'reference' => $this->getReference());
if ($this->getCashSession() === null) {
return ['cashSession' => null, 'reference' => $this->getReference];
} else {
return ['cashSession' => $this->getCashSession()->getId(),
'reference' => $this->getReference()];
}
}
/**
......
......@@ -55,9 +55,15 @@ class CashSessionCatTax extends DoctrineModel // Embedded class
}
public function getId() {
return ['cashSession' => $this->getCashSession()->getId(),
'reference' => $this->getReference(),
'tax' => $this->getTax()->getId()];
if ($this->getCashSession() === null) {
return ['cashSession' => null,
'reference' => $this->getReference(),
'tax' => $this->getTax()->getId()];
} else {
return ['cashSession' => $this->getCashSession()->getId(),
'reference' => $this->getReference(),
'tax' => $this->getTax()->getId()];
}
}
/**
......
......@@ -53,8 +53,13 @@ class CashSessionCustBalance extends DoctrineModel // Embedded class
];
}
public function getId() {
return array('cashSession' => $this->getCashSession()->getId(),
'customer' => $this->getCustomer()->getId());
if ($this->getCashSession() === null) {
return ['cashSession' => null,
'customer' => $this->getCustomer()->getId()];
} else {
return ['cashSession' => $this->getCashSession()->getId(),
'customer' => $this->getCustomer()->getId()];
}
}
/**
......
......@@ -56,9 +56,15 @@ class CashSessionPayment extends DoctrineModel // Embedded class
];
}
public function getId() {
return array('cashSession' => $this->getCashSession()->getId(),
if ($this->getCashSession() === null) {
return ['cashSession' => null,
'paymentMode' => $this->getPaymentMode()->getId(),
'currency' => $this->getCurrency()->getId()];
} else {
return ['cashSession' => $this->getCashSession()->getId(),
'paymentMode' => $this->getPaymentMode()->getId(),
'currency' => $this->getCurrency()->getId());
'currency' => $this->getCurrency()->getId()];
}
}
/**
......
......@@ -55,8 +55,13 @@ class CashSessionTax extends DoctrineModel // Embedded class
];
}
public function getId() {
return array('cashSession' => $this->getCashSession()->getId(),
'tax' => $this->getTax()->getId());
if ($this->getCashSession() === null) {
return ['cashSession' => null,
'tax' => $this->getTax()->getId()];
} else {
return ['cashSession' => $this->getCashSession()->getId(),
'tax' => $this->getTax()->getId()];
}
}
/**
......
......@@ -49,8 +49,15 @@ class CompositionProduct extends DoctrineModel
];
}
public function getId() { return ['compositionGroup' => $this->getCompositionGroup()->getId(),
'product' => $this->getProduct()->getId()]; }
public function getId() {
if ($this->getCompositionGroup() === null) {
return ['compositionGroup' => $this->getCompositionGroup()->getId(),
'product' => $this->getProduct()->getId()];
} else {
return ['compositionGroup' => $this->getCompositionGroup()->getId(),
'product' => $this->getProduct()->getId()];
}
}
/**
* @var integer
......
......@@ -107,5 +107,9 @@ class Floor extends DoctrineModel
$this->places->add($place);
$place->setFloor($this);
}
public function removePlace($place) {
$this->places->removeElement($place);
$place->setFloor(null);
}
}
......@@ -148,6 +148,10 @@ class PaymentMode extends DoctrineModel
public function clearValues() {
$this->values->clear();
}
public function removeValue($value) {
$this->values->removeElement($value);
$value->setPaymentMode(null);
}
/**
* @OneToMany(targetEntity="\Pasteque\Server\Model\PaymentModeReturn", mappedBy="paymentMode", cascade={"persist"}, orphanRemoval=true) */
......@@ -169,6 +173,10 @@ class PaymentMode extends DoctrineModel
public function clearReturns() {
$this->returns->clear();
}
public function removeReturn($return) {
$this->returns->removeElement($return);
$return->setPaymentMode(null);
}
/**
* True if an image can be found for this model.
......
......@@ -52,9 +52,13 @@ class PaymentModeReturn extends DoctrineModel // Embedded class
}
public function getId() {
if ($this->getPaymentMode() === null) { return null; }
return array('paymentMode' => $this->getPaymentMode()->getId(),
'minAmount' =>$this->getMinAmount());
if ($this->getPaymentMode() === null) {
return ['paymentMode' => null,
'minAmount' => $this->getMinAmount()];
} else {
return ['paymentMode' => $this->getPaymentMode()->getId(),
'minAmount' =>$this->getMinAmount()];
}
}
/**
......
......@@ -50,9 +50,12 @@ class PaymentModeValue extends DoctrineModel // Embedded class
}
public function getId() {
if ($this->getPaymentMode() === null) { return null; }
return array('paymentMode' => $this->getPaymentMode()->getId(),
'value' =>$this->getValue());
if ($this->getPaymentMode() === null) {
return ['paymentMode' => null, 'value' => $this->getValue()];
} else {
return ['paymentMode' => $this->getPaymentMode()->getId(),
'value' =>$this->getValue()];
}
}
/**
......
......@@ -123,5 +123,9 @@ class TariffArea extends DoctrineModel
$this->prices->add($price);
$price->setTariffArea($this);
}
public function removePrice($price) {
$this->prices->removeElement($price);
$price->setTariffArea(null);
}
}
......@@ -57,9 +57,13 @@ class TariffAreaPrice extends DoctrineModel // Embedded class
];
}
public function getId() {
if ($this->getTariffArea() === null) { return null; }
return array('tariffArea' => $this->getTariffArea()->getId(),
'product' =>$this->getProduct()->getId());
if ($this->getTariffArea() === null) {
return ['tariffArea' => null,
'product' => $this->getProduct()->getId()];
} else {
return ['tariffArea' => $this->getTariffArea()->getId(),
'product' =>$this->getProduct()->getId()];
}
}
/**
......
......@@ -61,8 +61,12 @@ class TicketLine extends DoctrineModel // Embedded class
];
}
public function getId() {
return array('ticket' => $this->getTicket()->getId(),
'dispOrder' => $this->getDispOrder());
if ($this->getTicket() === null) {
return ['ticket' => null, 'dispOrder' => $this->getDispOrder()];
} else {
return ['ticket' => $this->getTicket()->getId(),
'dispOrder' => $this->getDispOrder()];
}
}
/**
......
......@@ -56,8 +56,12 @@ class TicketPayment extends DoctrineModel // Embedded class
];
}
public function getId() {
return array('ticket' => $this->getTicket()->getId(),
'dispOrder' => $this->getDispOrder());
if ($this->getTicket() === null) {
return ['ticket' => null, 'dispOrder' => $this->getDispOrder()];
} else {
return ['ticket' => $this->getTicket()->getId(),
'dispOrder' => $this->getDispOrder()];
}
}
/**
......
......@@ -53,8 +53,12 @@ class TicketTax extends DoctrineModel // Embedded class
];
}
public function getId() {
return array('ticket' => $this->getTicket()->getId(),
'tax' => $this->getTax()->getId());
if ($this->getTicket() === null) {
return ['ticket' => null, 'tax' => $this->getTax()->getId()];
} else {
return ['ticket' => $this->getTicket()->getId(),
'tax' => $this->getTax()->getId()];
}
}
/**
......
......@@ -44,6 +44,23 @@ abstract class DoctrineModel
// Required for toStruct for references. */
public function getId() { return $this->id; }
public function idEquals($otherModel) {
$myId = $this->getId();
$otherId = $otherModel->getId();
if (!is_array($myId) && !is_array($otherId)) {
return $myId === $otherId;
} else if (is_array($myId) && is_array($otherId)) {
foreach ($myId as $key => $value) {
if (!isset($otherId[$key]) || $value !== $otherId[$key]) {
return false;
}
}
return true;
} else {
return false;
}
}
protected function directFieldToStruct($field) {
$value = call_user_func(array($this, 'get' . ucfirst($field)));
switch (gettype($value)) {
......@@ -83,6 +100,9 @@ abstract class DoctrineModel
/** Unlink the model from DAO and all methods.
* All references are converted to their Id.
* WARNING: Use to/fromStruct only once and do never switch back and forth
* from struct to DoctrineModel, as it may makes Doctrine mess up with the
* ids and duplicate or delete some records.
* @return An associative array with raw data, suitable for
* json encoding. */
public function toStruct() {
......@@ -150,6 +170,7 @@ abstract class DoctrineModel
} else {
if (!empty($field['embedded'])) {
$fromStruct = new \ReflectionMethod($field['class'], 'fromStruct');
$structId = null;
$submodel = $fromStruct->invoke(null, $struct[$field['name']], $dao, true);
} else {
$submodel = $dao->read($field['class'], $struct[$field['name']]);
......@@ -163,6 +184,9 @@ abstract class DoctrineModel
/** Build a model linked to the DAO from raw data.
* @param $struct An associative array containing the data
* (hasImage is a special field and is ignored from the struct).
* WARNING: Use to/fromStruct only once and do never switch back and forth
* from struct to DoctrineModel, as it may makes Doctrine mess up with the
* ids and duplicate or delete some records.
* @param $dao The DAO to link to.
* @param $embedded Only for recursive calls, don't set it.
* @throw \UnexpectedValueException If Id is set but not data is found.
......@@ -189,11 +213,70 @@ abstract class DoctrineModel
if (!empty($struct[$fieldName])) {
$value = static::readAssociationValue($struct, $dao, $field);
if (!empty($field['array'])) {
// At that point values are loaded from database by Doctrine
// Clear and rebuild
call_user_func(array($model, 'clear' . ucfirst($fieldName)));
/* At that point the model is filled from database by Doctrine.
* We must remove records that are not listed in struct
* and add those that are new.
* Unbind/rebind doesn't work because it makes Doctrine ignore
* the id. */
$toAdd = [];
$toRemove = [];
// Look for records to remove
$dbValues = call_user_func(array($model, 'get' . ucfirst($fieldName)));
foreach ($dbValues as $dbv) {
$found = false;
foreach ($value as $v) {
if ($dbv->idEquals($v)) {
$found = true;
continue;
}
}
if (!$found) {
$toRemove[] = $dbv;
}
}
// Look for records to add
foreach ($value as $v) {
$id = $v->getId();
if ($id == null) { // New record, add it.
$toAdd[] = $v;
continue;
} else {
// For non-incremental id, check if the record
// is already loaded or not. Add it if new.
$found = false;
foreach ($dbValues as $dbv) {
if ($dbv->idEquals($v)) {
$found = true;
break;
}
}
if (!$found) {
$toAdd[] = $v;
}
}
}
// Add and remove
foreach ($toRemove as $rm) {
$methodName = 'remove' . ucfirst($fieldName);
if (is_callable([$model, $methodName])) {
call_user_func([$model, $methodeName], $rm);
} else {
$methodName = substr($methodName, 0, -1);
call_user_func([$model, $methodName], $rm);
}
}
foreach ($toAdd as $add) {
$methodName = 'add' . ucfirst($fieldName);
if (is_callable([$model, $methodName])) {
call_user_func([$model, $methodName], $add);
} else {
$methodName = substr($methodName, 0, -1);
call_user_func([$model, $methodName], $add);
}
}
} else {
call_user_func(array($model, 'set' . ucfirst($fieldName)), $value);
}
call_user_func(array($model, 'set' . ucfirst($fieldName)), $value);
} else if (isset($struct[$fieldName]) && $struct[$fieldName] === null) {
// Delete association on explicit null value
if (empty($field['array'])) {
......
......@@ -261,7 +261,7 @@ class PaymentModeAPITest extends TestCase
$count = $this->api->count();
$this->assertEquals(2, $count, // There is pm2 so 2
'Payment mode was inserted instead of updated');
$read = $this->api->get($pmId);
$read = $this->dao->readSnapshot(PaymentMode::class, $pmId);
$this->assertEquals($newPm->getReference(), $read->getReference());
$this->assertEquals($newPm->getLabel(), $read->getLabel());
$this->assertEquals(1, $read->getValues()->count());
......
......@@ -111,7 +111,7 @@ class PlaceAPITest extends TestCase
$this->dao->commit();
$replaceFloor = Floor::fromStruct(['id' => $floor->getId(), 'label' => 'replaced', 'places' => [['label' => 'place2', 'x' => 30, 'y' => 20]]], $this->dao);
$this->api->write($replaceFloor);
$readFloor = $this->dao->read(Floor::class, $floor->getId());
$readFloor = $this->dao->readSnapshot(Floor::class, $floor->getId());
$allFloors = $this->dao->search(Floor::class);
$this->assertEquals(1, count($allFloors),
'Floor was inserted instead of updated');
......@@ -127,4 +127,34 @@ class PlaceAPITest extends TestCase
$this->assertEquals(30, $place->getX());
$this->assertEquals(20, $place->getY());
}
/** @depends testReplace */
public function testAdd() {
$floor = new Floor();
$floor->setLabel('Floor');
$place = new Place();
$place->setLabel('Place');
$place->setX(10);
$place->setY(5);
$floor->addPlace($place);
$this->dao->write($floor);
$this->dao->commit();
$place2 = new Place();
$place2->setLabel('Place2');
$place2->setX(20);
$place2->setY(25);
$floor->addPlace($place2);
$this->api->write($floor);
$readFloor = $this->dao->readSnapshot(Floor::class, $floor->getId());
$allFloors = $this->dao->search(Floor::class);
$this->assertEquals(1, count($allFloors),
'Floor was inserted instead of updated');
$this->assertEquals($floor->getId(), $readFloor->getId());
$allPlaces = $this->dao->search(Place::class);
$this->assertEquals(2, count($allPlaces),
'Places update failed');
$this->assertEquals(2, $readFloor->getPlaces()->count(),
'New place was not attached to the updated floor');
$this->markTestIncomplete('Check places content (no order)');
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment