commit 3cc8777ab1eb1cf5e07885071b7bea79dd776753
parent 9d3292e3048c87f1281ec8796484e292ddd4cc38
Author: Felicitus <felicitus@felicitus.org>
Date: Fri, 16 Mar 2012 12:59:26 +0100
Refactored the part manager to the "new" AbstractManager system and added the first unit tests for parts.
Diffstat:
9 files changed, 354 insertions(+), 175 deletions(-)
diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/CategoryNotAssignedException.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/CategoryNotAssignedException.php
@@ -0,0 +1,19 @@
+<?php
+namespace de\RaumZeitLabor\PartKeepr\Part\Exceptions;
+
+use de\RaumZeitLabor\PartKeepr\PartKeepr,
+ de\RaumZeitLabor\PartKeepr\Util\SerializableException;
+
+/**
+ * This exception is thrown when a part hasn't got a category assigned
+ */
+class CategoryNotAssignedException extends SerializableException {
+
+ /**
+ * Constructs the exception
+ * @param BaseEntity $entity
+ */
+ public function __construct () {
+ parent::__construct(PartKeepr::i18n("No category assigned"));
+ }
+}+
\ No newline at end of file
diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/StorageLocationNotAssignedException.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/Exceptions/StorageLocationNotAssignedException.php
@@ -0,0 +1,19 @@
+<?php
+namespace de\RaumZeitLabor\PartKeepr\Part\Exceptions;
+
+use de\RaumZeitLabor\PartKeepr\PartKeepr,
+ de\RaumZeitLabor\PartKeepr\Util\SerializableException;
+
+/**
+ * This exception is thrown when a part hasn't got a storage location assigned
+ */
+class StorageLocationNotAssignedException extends SerializableException {
+
+ /**
+ * Constructs the exception
+ * @param BaseEntity $entity
+ */
+ public function __construct () {
+ parent::__construct(PartKeepr::i18n("No storage location assigned"));
+ }
+}+
\ No newline at end of file
diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/Part.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/Part.php
@@ -9,12 +9,16 @@ use de\RaumZeitLabor\PartKeepr\StorageLocation\StorageLocation,
de\RaumZeitLabor\PartKeepr\Util\Serializable,
de\RaumZeitLabor\PartKeepr\Util\BaseEntity,
de\RaumZeitLabor\PartKeepr\PartKeepr,
- de\RaumZeitLabor\PartKeepr\Util\Exceptions\OutOfRangeException;
+ de\RaumZeitLabor\PartKeepr\Part\Exceptions\CategoryNotAssignedException,
+ de\RaumZeitLabor\PartKeepr\Util\Exceptions\OutOfRangeException,
+ de\RaumZeitLabor\PartKeepr\Part\Exceptions\StorageLocationNotAssignedException;
/**
* Represents a part in the database. The heart of our project. Handle with care!
- * @Entity **/
+ *
+ * @Entity @HasLifecycleCallbacks
+ */
class Part extends BaseEntity implements Serializable, Deserializable {
/**
* The category of the part
@@ -530,4 +534,53 @@ class Part extends BaseEntity implements Serializable, Deserializable {
}
}
}
+
+ /**
+ * Checks if the part category is set.
+ *
+ * @throws CategoryNotAssignedException
+ */
+ private function checkCategoryConsistency () {
+ if ($this->getCategory() === null) {
+ throw new CategoryNotAssignedException();
+ }
+ }
+
+ /**
+ * Checks if the part storage location is set.
+ *
+ * @throws StorageLocationNotAssignedException
+ */
+ private function checkStorageLocationConsistency () {
+ if ($this->getStorageLocation() === null) {
+ throw new StorageLocationNotAssignedException();
+ }
+ }
+
+ /**
+ * Checks if the requirements for database persisting are given.
+ *
+ * @throws CategoryNotAssignedException Thrown if no category is set
+ * @throws StorageLocationNotAssignedException Thrown if no storage location is set
+ *
+ * @PrePersist
+ */
+ public function onPrePersist () {
+ $this->checkCategoryConsistency();
+ $this->checkStorageLocationConsistency();
+ }
+
+ /**
+ *
+ * Checks if the requirements for database persisting are given.
+ *
+ * For a list of exceptions, see
+ * @see de\RaumZeitLabor\PartKeepr\Part.Part::onPrePersist()
+ *
+ * @PreUpdate */
+ public function onPreUpdate () {
+ $this->checkCategoryConsistency();
+ $this->checkStorageLocationConsistency();
+ }
+
}
diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/PartManager.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/PartManager.php
@@ -2,7 +2,10 @@
namespace de\RaumZeitLabor\PartKeepr\Part;
use de\RaumZeitLabor\PartKeepr\UploadedFile\TempUploadedFile,
+ de\RaumZeitLabor\PartKeepr\Manager\ManagerFilter,
+ Doctrine\ORM\QueryBuilder,
de\RaumZeitLabor\PartKeepr\PartParameter\PartParameter,
+ de\RaumZeitLabor\PartKeepr\Manager\AbstractManager,
de\RaumZeitLabor\PartKeepr\Unit\Unit,
de\RaumZeitLabor\PartKeepr\SiPrefix\SiPrefix,
de\RaumZeitLabor\PartKeepr\Part\PartDistributor,
@@ -22,120 +25,52 @@ use de\RaumZeitLabor\PartKeepr\UploadedFile\TempUploadedFile,
de\RaumZeitLabor\PartKeepr\PartCategory\PartCategoryManager,
de\RaumZeitLabor\PartKeepr\Manufacturer\ManufacturerManager;
-class PartManager extends Singleton {
+class PartManager extends AbstractManager {
+
+ /**
+ * Returns the FQCN for the target entity to operate on.
+ * @return string The FQCN, e.g. de\RaumZeitLabor\PartKeepr\Part
+ */
+ public function getEntityName () {
+ return 'de\RaumZeitLabor\PartKeepr\Part\Part';
+ }
+
+ /**
+ * Returns all fields which need to appear in the getList ResultSet.
+ * @return array An array of all fields which should be returned
+ */
+ public function getQueryFields () {
+ return array("name", "averagePrice", "status", "needsReview", "createDate", "id", "stockLevel",
+ "minStockLevel", "comment", "st.id AS storageLocation_id", "categoryPath",
+ "st.name as storageLocationName", "f.id AS footprint_id", "f.name AS footprintName",
+ "c.id AS category", "c.name AS categoryName", "pu.id AS partUnit", "pu.name AS partUnitName",
+ "pu.is_default AS partUnitDefault"
+ );
+ }
+
+ /**
+ * Returns the default sort field
+ *
+ * @return string The default sort field
+ */
+ public function getDefaultSortField () {
+ return "dateTime";
+ }
+
/**
- * Returns a list of parts.
- *
- * @todo The parameter list. We need to invent something so that we don't have like 20 parameters for this method.
+ * Appends various join tables to the result set
+ *
+ * (non-PHPdoc)
+ * @see de\RaumZeitLabor\PartKeepr\Manager.AbstractManager::applyCustomQuery()
*/
- public function getParts ($start = 0, $limit = 10, $sort = null, $filter = "", $category = 0, $categoryScope = "all", $stockMode = "all", $withoutPrice = false, $storageLocation = "") {
-
- $qb = PartKeepr::getEM()->createQueryBuilder();
- $qb->select("COUNT(p.id)")->from("de\RaumZeitLabor\PartKeepr\Part\Part","p")
- ->join("p.storageLocation", "st")
- ->leftJoin("p.footprint", "f")
- ->join("p.category", "c")
- ->leftJoin("p.partUnit", "pu");
-
-
- $qb->where("1=1");
- if ($filter != "") {
- $qb = $qb->where("LOWER(p.name) LIKE :filter");
- $qb->setParameter("filter", "%".strtolower($filter)."%");
- }
-
- if ($storageLocation !== null) {
- /* If storage location is empty, assume new record. This isn't nice and to be considered as a hack */
- if ($storageLocation == "") {
- return array();
- }
- $qb->andWhere("st.name = :storageLocation");
- $qb->setParameter("storageLocation", $storageLocation);
- }
-
- $category = intval($category);
-
-
-
- if ($category !== 0) {
- /* Fetch all children */
- if ($categoryScope == "selected") {
- $qb->andWhere("p.category = :category");
- $qb->setParameter("category", $category);
- } else {
- $childs = PartCategoryManager::getInstance()->getChildNodes($category);
- $childs[] = $category;
- $qb->andWhere("p.category IN (".implode(",", $childs).")");
- }
- }
-
- switch ($stockMode) {
- case "all":
- break;
- case "zero":
- $qb->andWhere("p.stockLevel = 0");
- break;
- case "nonzero":
- $qb->andWhere("p.stockLevel > 0");
- break;
- case "below":
- $qb->andWhere("p.stockLevel < p.minStockLevel");
- break;
- }
-
- if ($withoutPrice === true || $withoutPrice === "true") {
- $qb->andWhere("p.averagePrice IS NULL");
- }
-
- $totalQuery = $qb->getQuery();
-
-
-
-
- $qb->select("p.averagePrice, p.status, p.name, p.needsReview, p.createDate, p.id, p.stockLevel, p.minStockLevel, p.comment, st.id AS storageLocation_id, p.categoryPath, st.name as storageLocationName, f.id AS footprint_id, f.name AS footprintName, c.id AS category, c.name AS categoryName, pu.id AS partUnit, pu.name AS partUnitName, pu.is_default AS partUnitDefault");
- if ($sort === null) {
- $qb->addOrderBy("p.name", "ASC");
- } else {
- $sortArray = json_decode($sort, true);
-
- foreach ($sortArray as $sortParam) {
- switch ($sortParam["property"]) {
- case "storageLocationName":
- $orderBy = "st.name";
- break;
- case "footprintName":
- $orderBy = "f.name";
- break;
- default;
- $orderBy = "p.".$sortParam["property"];
- break;
- }
-
- $qb->addOrderBy($orderBy, $sortParam["direction"]);
- }
- }
-
-
- if ($limit > -1) {
- $qb->setMaxResults($limit);
- $qb->setFirstResult($start);
- }
-
- $query = $qb->getQuery();
-
- $result = $query->getArrayResult();
-
- foreach ($result as $key => $item) {
- $dql = "SELECT COUNT(pa) FROM de\RaumZeitLabor\PartKeepr\Part\PartAttachment pa WHERE pa.part = :part";
- $query = PartKeepr::getEM()->createQuery($dql);
- $query->setParameter("part", $item["id"]);
-
- $result[$key]["attachmentCount"] = $query->getSingleScalarResult();
- }
-
-
-
- return array("data" => $result, "totalCount" => $totalQuery->getSingleScalarResult());
+ protected function applyCustomQuery (QueryBuilder $qb, ManagerFilter $filter) {
+ /**
+ * Pull in additional tables
+ */
+ $qb ->join("q.storageLocation", "st")
+ ->leftJoin("q.footprint", "f")
+ ->join("q.category", "c")
+ ->leftJoin("q.partUnit", "pu");
}
public function addOrUpdatePart ($aParameters) {
diff --git a/src/backend/de/RaumZeitLabor/PartKeepr/Part/PartService.php b/src/backend/de/RaumZeitLabor/PartKeepr/Part/PartService.php
@@ -4,10 +4,12 @@ namespace de\RaumZeitLabor\PartKeepr\Part;
use de\RaumZeitLabor\PartKeepr\User\User,
de\RaumZeitLabor\PartKeepr\Service\RestfulService,
de\RaumZeitLabor\PartKeepr\Service\Service,
+ de\RaumZeitLabor\PartKeepr\Manager\ManagerFilter,
de\RaumZeitLabor\PartKeepr\Part\PartManager,
de\RaumZeitLabor\PartKeepr\Stock\StockEntry,
de\RaumZeitLabor\PartKeepr\PartKeepr,
de\RaumZeitLabor\PartKeepr\PartCategory\PartCategory,
+ de\RaumZeitLabor\PartKeepr\PartCategory\PartCategoryManager,
de\RaumZeitLabor\PartKeepr\Session\SessionManager;
class PartService extends Service implements RestfulService {
@@ -15,60 +17,113 @@ class PartService extends Service implements RestfulService {
if ($this->hasParameter("id")) {
return array("data" => PartManager::getInstance()->getPart($this->getParameter("id"))->serialize());
} else {
- return PartManager::getInstance()->getParts(
- $this->getParameter("start", $this->getParameter("start", 0)),
- $this->getParameter("limit", $this->getParameter("limit", 25)),
- $this->getParameter("sort", $this->getParameter("sort")),
- $this->getParameter("query", ""),
- $this->getParameter("category", 0),
- $this->getParameter("categoryScope", "all"),
- $this->getParameter("stockMode", "all"),
- $this->getParameter("withoutPrice", false),
- $this->getParameter("storageLocation", null));
+
+ $filter = new ManagerFilter($this);
+ $filter->setFilterCallback(array($this, "filterCallback"));
+
+ return PartManager::getInstance()->getList($filter);
}
}
+ /**
+ * Advanced filtering for the list
+ * @param QueryBuilder The $queryBuilder
+ */
+ public function filterCallback ($queryBuilder) {
+
+ /**
+ * Applies text-based filtering
+ */
+ if ($this->hasParameter("query") && $this->getParameter("query") != "") {
+ $queryBuilder->where("LOWER(q.name) LIKE :filter");
+ $queryBuilder->setParameter("filter", "%".strtolower($this->getParameter("query"))."%");
+ }
+
+ /**
+ * Applies filtering by the storage location name
+ */
+ if ($this->getParameter("storageLocation") !== null) {
+ $queryBuilder->andWhere("st.name = :storageLocation");
+ $queryBuilder->setParameter("storageLocation", $this->getParameter("storageLocation"));
+ }
+
+ /**
+ * Filter by the category id and set the category mode
+ *
+ */
+ $category = intval($this->getParameter("category", 0));
+
+ if ($category !== 0) {
+ /* Fetch all children */
+ if ($this->getParameter("categoryScope") == "selected") {
+ $queryBuilder->andWhere("q.category = :category");
+ $queryBuilder->setParameter("category", $category);
+ } else {
+ $childs = PartCategoryManager::getInstance()->getChildNodes($category);
+ $childs[] = $category;
+ $queryBuilder->andWhere("q.category IN (".implode(",", $childs).")");
+ }
+ }
+
+ /**
+ * Filter by the stock mode
+ */
+ switch ($this->getParameter("stockMode")) {
+ case "all":
+ break;
+ case "zero":
+ $queryBuilder->andWhere("q.stockLevel = 0");
+ break;
+ case "nonzero":
+ $queryBuilder->andWhere("q.stockLevel > 0");
+ break;
+ case "below":
+ $queryBuilder->andWhere("q.stockLevel < q.minStockLevel");
+ break;
+ }
+
+ /**
+ * Filter by the price
+ */
+ if ($this->getParameter("withoutPrice") === true || $this->getParameter("withoutPrice") === "true") {
+ $queryBuilder->andWhere("q.averagePrice IS NULL");
+ }
+ }
+
/**
* (non-PHPdoc)
* @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::create()
*/
public function create () {
- $part = new Part();
- $part->deserialize($this->getParameters());
-
- PartKeepr::getEM()->persist($part);
- PartKeepr::getEM()->flush();
-
- if ($this->getParameter("initialStockLevel") > 0) {
-
- try {
- $user = User::loadById($this->getParameter("initialStockLevelUser"));
- } catch (\Exception $e) {
- $user = SessionManager::getCurrentSession()->getUser();
- }
-
- $stock = new StockEntry($part, intval($this->getParameter("initialStockLevel")), $user);
-
- if ($this->getParameter("initialStockLevelPricePerItem") == true) {
- $price = floatval($this->getParameter("initialStockLevelPrice"));
- } else {
- $price = floatval($this->getParameter("initialStockLevelPrice")) / $this->getParameter("initialStockLevel");
- }
-
- if ($price != 0) {
- $stock->setPrice($price);
- }
-
- PartKeepr::getEM()->persist($stock);
- PartKeepr::getEM()->flush();
-
- $part->updateStockLevel();
- PartKeepr::getEM()->flush();
-
- return array("data" => $part->serialize());
+ $entity = PartManager::getInstance()->createEntity($this->getParameters());
+
+ if ($this->getParameter("initialStockLevel") > 0) {
+ try {
+ $user = User::loadById($this->getParameter("initialStockLevelUser"));
+ } catch (\Exception $e) {
+ $user = SessionManager::getCurrentSession()->getUser();
+ }
+
+ $stock = new StockEntry($entity, intval($this->getParameter("initialStockLevel")), $user);
+
+ if ($this->getParameter("initialStockLevelPricePerItem") == true) {
+ $price = floatval($this->getParameter("initialStockLevelPrice"));
+ } else {
+ $price = floatval($this->getParameter("initialStockLevelPrice")) / $this->getParameter("initialStockLevel");
+ }
+
+ if ($price != 0) {
+ $stock->setPrice($price);
+ }
+
+ PartKeepr::getEM()->persist($stock);
+ PartKeepr::getEM()->flush();
+
+ $part->updateStockLevel();
+ PartKeepr::getEM()->flush();
}
- return array("data" => $part->serialize());
+ return array("data" => $entity->serialize());
}
/**
@@ -76,13 +131,12 @@ class PartService extends Service implements RestfulService {
* @see de\RaumZeitLabor\PartKeepr\Service.RestfulService::update()
*/
public function update () {
- $this->requireParameter("id");
- $part = Part::loadById($this->getParameter("id"));
- $part->deserialize($this->getParameters());
-
- PartKeepr::getEM()->flush();
-
- return array("data" => $part->serialize());
+ $entity = PartManager::getInstance()->getEntity($this->getParameter("id"));
+ $entity->deserialize($this->getParameters());
+
+ PartKeepr::getEM()->flush();
+
+ return array("data" => $entity->serialize());
}
@@ -116,7 +170,6 @@ class PartService extends Service implements RestfulService {
}
-
PartKeepr::getEM()->flush();
}
diff --git a/tests/Auth/UserTest.php b/tests/Auth/UserTest.php
@@ -2,7 +2,7 @@
namespace de\RaumZeitLabor\PartKeepr\Tests\Auth;
declare(encoding = 'UTF-8');
-use de\RaumZeitLabor\PartKeepr\Auth\User;
+use de\RaumZeitLabor\PartKeepr\User\User;
class UserTest extends \PHPUnit_Framework_TestCase {
public function testBasics () {
@@ -13,11 +13,11 @@ class UserTest extends \PHPUnit_Framework_TestCase {
$user->setUsername("Timo A. Hummel");
- $hashedPassword = "3858f62230ac3c915f300c664312c63f";
-
$this->assertEquals($user->getUsername(), "timo_a_hummel");
$user->setPassword("foobar");
+ $hashedPassword = "3858f62230ac3c915f300c664312c63f";
+
$this->assertEquals($user->comparePassword("foobar"), true, "Error comparing passwords: PasswordTest01");
$this->assertEquals($user->compareHashedPassword($hashedPassword), true, "Error comparing passwords: PasswordTest02");
@@ -26,4 +26,3 @@ class UserTest extends \PHPUnit_Framework_TestCase {
$this->assertEquals($user->compareHashedPassword($hashedPassword), true, "Error comparing passwords: PasswordTest04");
}
}
-?>-
\ No newline at end of file
diff --git a/tests/Part/PartServiceTest.php b/tests/Part/PartServiceTest.php
@@ -0,0 +1,80 @@
+<?php
+namespace de\RaumZeitLabor\PartKeepr\Tests\Part;
+
+use de\RaumZeitLabor\PartKeepr\PartCategory\PartCategoryManager,
+ de\RaumZeitLabor\PartKeepr\Part\PartService,
+ de\RaumZeitLabor\PartKeepr\PartKeepr,
+ de\RaumZeitLabor\PartKeepr\Part\Part,
+ de\RaumZeitLabor\PartKeepr\StorageLocation\StorageLocationManager,
+ de\RaumZeitLabor\PartKeepr\StorageLocation\StorageLocationService;
+
+class PartServiceTest extends \PHPUnit_Framework_TestCase {
+ protected $backupGlobals = false;
+
+ private static $storageLocation;
+
+ public static function setUpBeforeClass () {
+ // Create a storage location for testing
+ $storageLocation = array("name" => "PartServiceTest");
+
+ $service = new StorageLocationService($storageLocation);
+ $result = $service->create();
+
+ self::$storageLocation = $result["data"]["id"];
+ }
+
+ /**
+ * @expectedException de\RaumZeitLabor\PartKeepr\Part\Exceptions\CategoryNotAssignedException
+ */
+ public function testCreatePartWithoutCategory () {
+ $partName = "testCreatePartWithoutCategory";
+
+ $part = array(
+ "name" => $partName,
+ "storageLocation" => self::$storageLocation
+ );
+
+ $service = new PartService($part);
+ $service->create();
+ }
+
+ /**
+ * @expectedException de\RaumZeitLabor\PartKeepr\Part\Exceptions\StorageLocationNotAssignedException
+ */
+ public function testCreatePartWithoutStorageLocation () {
+ $partName = "testCreatePartWithoutStorageLocation";
+
+ $part = array(
+ "name" => $partName,
+ "category" => 1
+ );
+
+ $service = new PartService($part);
+ $service->create();
+ }
+
+ /**
+ * Verifies that querying for a part works correctly.
+ *
+ * There was a bug where only lower-case strings matched against parts, fixed 2012-03-16.
+ */
+ public function testPartNameQuerying () {
+ $partName = "testPartNameQuerying";
+
+
+ $part = new Part();
+ $part->setName($partName);
+ $part->setCategory(PartCategoryManager::getInstance()->getRootNode()->getNode());
+ $part->setStorageLocation(StorageLocationManager::getInstance()->getStorageLocation(self::$storageLocation));
+
+ PartKeepr::getEM()->persist($part);
+ PartKeepr::getEM()->flush();
+
+ $service = new PartService(array("query" => $partName));
+
+ $response = $service->get();
+
+ $this->assertEquals(1, $response["totalCount"], "The resultset totalCount is wrong.");
+ $this->assertEquals($response["data"][0]["name"], $part->getName());
+ }
+}
diff --git a/tests/Service/ServiceTest.php b/tests/Service/ServiceTest.php
@@ -0,0 +1,20 @@
+<?php
+namespace de\RaumZeitLabor\PartKeepr\Tests\Service;
+
+use de\RaumZeitLabor\PartKeepr\Service\Service,
+ de\RaumZeitLabor\PartKeepr\PartKeepr,
+ de\RaumZeitLabor\PartKeepr\Part\Part;
+
+
+class ServiceTest extends \PHPUnit_Framework_TestCase {
+ protected $backupGlobals = false;
+
+ /**
+ * Tests injection of service parameters via the constructor.
+ */
+ public function testConstructorParameterPassing () {
+ $service = new Service(array("foo" => "bar"));
+
+ $this->assertEquals($service->getParameter("foo"), "bar");
+ }
+}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php
@@ -1,8 +1,8 @@
<?php
namespace de\RaumZeitLabor\PartKeepr\Tests;
-declare(encoding = 'UTF-8');
-use de\raumzeitlabor\PartKeepr\PartKeepr;
+use de\RaumZeitLabor\PartKeepr\PartCategory\PartCategoryManager,
+ de\raumzeitlabor\PartKeepr\PartKeepr;
include(dirname(__DIR__). "/src/backend/de/RaumZeitLabor/PartKeepr/PartKeepr.php");
@@ -14,4 +14,4 @@ $classes = PartKeepr::getClassMetaData();
$tool->dropSchema($classes);
$tool->createSchema($classes);
-?>-
\ No newline at end of file
+PartCategoryManager::getInstance()->ensureRootExists();+
\ No newline at end of file