From: Michael Wallner Date: Sat, 19 Sep 2015 07:05:50 +0000 (+0200) Subject: data mapper POC X-Git-Url: https://git.m6w6.name/?p=m6w6%2Fpq-gateway;a=commitdiff_plain;h=e6da4c7430e5c5b4ba482206e42aeebb4363d2b3 data mapper POC --- diff --git a/.gitignore b/.gitignore index ff6a0c3..9b81c05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /nbproject/ tests/query.log +vendor diff --git a/README.md b/README.md index 0b8d5cd..1ac5b4c 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,18 @@ for [ext-pq](http://git.php.net/?p=pecl/database/pq.git;a=summary). http://devel-m6w6.rhcloud.com/mdref/pq-gateway ## News +* ***2015-05-20:*** 2.1.0 tagged * ***2014-10-15:*** 2.0.0 tagged * ***2013-05-15:*** 1.1.0 tagged * ***2013-05-03:*** 1.0.0 tagged ## ChangeLog +### 2.1.0 +* Added pq\Query\AsyncExecutor::setCallbacks(callable $init, callable $done, callable $then) + and removed soft dependency on reactphp/promise +* Fixed pq\Gateway\Table::with()'s relation handling when source table equals foreign table + ### 2.0.0 * Published documentation * Added support for pecl/pq-0.5 diff --git a/composer.json b/composer.json index 2fd2af6..5c87f36 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "autoload": { "psr-0": { "pq\\Gateway": "lib", - "pq\\Query": "lib" + "pq\\Query": "lib", + "pq\\Data": "lib" } }, "require-dev": { diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..7489369 --- /dev/null +++ b/composer.lock @@ -0,0 +1,122 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "813817d14a924e8b4189c1365120cf86", + "packages": [], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v1.0.0-beta4", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "9fa6010f192f82a81381ae2dd372e1e75107d332" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/9fa6010f192f82a81381ae2dd372e1e75107d332", + "reference": "9fa6010f192f82a81381ae2dd372e1e75107d332", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "~4.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.0-dev", + "dev-v1.0.x": "1.0.0-dev" + } + }, + "autoload": { + "psr-4": { + "Amp\\": "lib/" + }, + "files": [ + "lib/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net", + "role": "Creator / Lead Developer" + } + ], + "description": "A non-blocking concurrency framework for PHP applications", + "homepage": "https://github.com/amphp/amp", + "keywords": [ + "async", + "concurrency", + "event", + "future", + "non-blocking", + "promise" + ], + "time": "2015-05-20 03:12:32" + }, + { + "name": "react/promise", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "365fcee430dfa4ace1fbc75737ca60ceea7eeeef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/365fcee430dfa4ace1fbc75737ca60ceea7eeeef", + "reference": "365fcee430dfa4ace1fbc75737ca60ceea7eeeef", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "React\\Promise\\": "src/" + }, + "files": [ + "src/functions_include.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@googlemail.com" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "time": "2014-12-30 13:32:42" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "amphp/amp": 10 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/lib/pq/Gateway/Table/Reference.php b/lib/pq/Gateway/Table/Reference.php index d91d9c7..6fcb7e3 100644 --- a/lib/pq/Gateway/Table/Reference.php +++ b/lib/pq/Gateway/Table/Reference.php @@ -36,7 +36,7 @@ class Reference implements \IteratorAggregate * @param array $ref */ function __construct($ref) { - $this->name = self::name($ref); + $this->name = self::name($ref["foreignColumns"], $ref["referencedColumns"]); $this->foreignTable = $ref["foreignTable"]; $this->foreignColumns = $ref["foreignColumns"]; $this->referencedTable = $ref["referencedTable"]; @@ -53,13 +53,14 @@ class Reference implements \IteratorAggregate /** * Compose an identifying name - * @param array $ref + * @param array $foreignColumns + * @param array $referencedColumns * @return string */ - static function name($ref) { + static function name(array $foreignColumns, array $referencedColumns) { return implode("_", array_map(function($ck, $cr) { return preg_replace("/_$cr\$/", "", $ck); - }, $ref["foreignColumns"], $ref["referencedColumns"])); + }, $foreignColumns, $referencedColumns)); } /** diff --git a/lib/pq/Mapper/Map.php b/lib/pq/Mapper/Map.php new file mode 100644 index 0000000..834ffb1 --- /dev/null +++ b/lib/pq/Mapper/Map.php @@ -0,0 +1,198 @@ +class = $class; + $this->gateway = $gateway; + $this->properties = $properties; + foreach ($properties as $property) { + $property->setContainer($this); + } + $this->objects = new ObjectCache($this); + } + + function getClass() { + return $this->class; + } + + function getObjects() { + return $this->objects; + } + + /** + * @return Table + */ + function getGateway() { + return $this->gateway; + } + + function getProperties() { + return $this->properties; + } + + function addProperty(PropertyInterface $property) { + $property->setContainer($this); + $this->properties[] = $property; + return $this; + } +/* + function idOf(Row $row, $check = false) { + $identity = $row->getIdentity(); + if (is_scalar($identity)) { + return $identity; + } + + if ($check && !isset($identity)) { + return false; + } + + if (is_array($identity)) { + if ($check && array_search(null, $identity, true)) { + return false; + } + /* one level is better than no level * / + asort($identity); + } + return json_encode($identity); + } + + function objectOf(Row $row) { + $id = $this->idOf($row); + + if (isset($this->objects["obj"][$id])) { + $obj = $this->objects["obj"][$id]; + } else { + $obj = new $this->class; + $this->objects["obj"][$id] = $obj; + $this->objects["row"][spl_object_hash($obj)] = $row; + } + return $obj; + } + + function rowOf($object) { + $id = spl_object_hash($object); + + if (isset($this->objects["row"][$id])) { + $row = $this->objects["row"][$id]; + } else { + $row = new Row($this->gateway); + $this->objects["row"][$id] = $row; + } + return $row; + } +*/ + function allOf(Row $row, $refName, &$objects = null) { + /* apply objectOf to populate the object cache */ + return $this->gateway->of($row, $refName)->apply(function($row) use(&$objects) { + $objects[] = $this->objects->asObject($row); + }); + } + + function refOf(Row $row, $refName, &$objects = null) { + $rid = []; + $rel = $row->getTable()->getRelation($this->gateway->getName(), $refName); + // FIXME: check if foreign key points to primary key + foreach ($rel as $fgn => $col) { + $rid[$col] = $row->$fgn->get(); + } + $rid = $this->objects->serializeRowId($rid); + if ($this->objects->hasObject($rid)) { + $object = $this->objects->getObjectById($rid); + $row = $this->objects->getRow($object); + $objects[] = $object; + $rowset = new Rowset($this->gateway); + return $rowset->append($row); + } + /* apply objectOf to populate the object cache */ + return $this->gateway->by($row, $refName)->apply(function($row) use(&$objects) { + $objects[] = $this->objects->asObject($row); + }); + } + + function relOf(MapInterface $map, $refName) { + return $map->getGateway()->getRelation( + $this->gateway->getName(), $refName); + } + + private function drain(array $deferred, callable $exec) { + while ($deferred) { + $cb = array_shift($deferred); + if (($cb = $exec($cb))) { + $deferred[] = $cb; + } + } + } + + function map(Row $row) { + $deferred = []; + $object = $this->objects->asObject($row); + foreach ($this->properties as $property) { + if (($cb = $property->read($row, $object))) { + $deferred[] = $cb; + } + } + $this->drain($deferred, function(callable $cb) use($row, $object) { + return $cb($row, $object); + }); + return $object; + } + + function mapAll(Rowset $rows) { + $objects = []; + foreach ($rows as $row) { + $objects[] = $this->map($row); + } + return $objects; + } + + function unmap($object) { + $deferred = []; + /* @var $row Row */ + $row = $this->objects->asRow($object); + $upd = $this->objects->rowId($row, true); + foreach ($this->properties as $property) { + if (($cb = $property->write($object, $row))) { + $deferred[] = $cb; + } + } + foreach ($this->gateway->getIdentity() as $col) { + if (null === $row->$col->get() + || ($row->$col->isExpr() && $row->$col->get()->isNull())) + { + $row->$col = new Expr("DEFAULT"); + } + } + if ($row->isDirty()) { + if ($upd) { + $row->update(); + } else { + $row->create(); + } + } + foreach ($this->properties as $property) { + if (($cb = $property->read($row, $object))) { + $deferred[] = $cb; + } + } + $this->drain($deferred, function($cb) use($object, $row) { + return $cb($object, $row); + }); + if ($row->isDirty()) { + $row->update(); + } + } + +} \ No newline at end of file diff --git a/lib/pq/Mapper/MapInterface.php b/lib/pq/Mapper/MapInterface.php new file mode 100644 index 0000000..0c3430c --- /dev/null +++ b/lib/pq/Mapper/MapInterface.php @@ -0,0 +1,48 @@ +maps[$map->getClass()] = $map; + return $this; + } + + function getReflector($class, $prop) { + if (is_object($class)) { + $class = get_class($class); + } + $hash = "$class::$prop"; + if (!isset($this->refp[$hash])) { + $this->refp[$hash] = new \ReflectionProperty($class, $prop); + $this->refp[$hash]->setAccessible(true); + } + return $this->refp[$hash]; + } + + /** + * @param string $class + * @return \pq\Mapper\MapInterface + * @throws UnexpectedValueException + */ + function mapOf($class) { + if (is_object($class)) { + $class = get_class($class); + } + if (!isset($this->maps[$class])) { + if (!is_callable([$class, "mapAs"])) { + throw new UnexpectedValueException("Not a mapped class: '$class'"); + } + $this->register($class::mapAs($this)); + } + return $this->maps[$class]; + } + + /** + * @param string $class + * @return \pq\Mapper\Storage + */ + function createStorage($class) { + return new Storage($this, $class); + } + + /** + * @param string $property + * @param string $field + * @return \pq\Mapper\Property\Field + */ + function mapField($property, $field = null) { + return new Property\Field($this, $property, $field); + } + + /** + * @param string $property + * @return \pq\Mapper\Property\All + */ + function mapAll($property) { + return new Property\All($this, $property); + } + + /** + * @param string $property + * @return \pq\Mapper\Property\Ref + */ + function mapRef($property) { + return new Property\Ref($this, $property); + } +} diff --git a/lib/pq/Mapper/ObjectCache.php b/lib/pq/Mapper/ObjectCache.php new file mode 100644 index 0000000..777dd51 --- /dev/null +++ b/lib/pq/Mapper/ObjectCache.php @@ -0,0 +1,133 @@ +map = $map; + } + + function reset() { + $this->obj = []; + $this->row = []; + } + + function rowId(Row $row, $check = false) { + try { + $identity = $row->getIdentity(); + } catch (OutOfBoundsException $e) { + return false; + } + return $this->serializeRowId($identity, $check); + } + + function objectId($object) { + return spl_object_hash($object); + } + + function extractRowId($object) { + $id = []; + foreach ($this->map->getGateway()->getIdentity() as $col) { + foreach ($this->map->getProperties() as $property) { + if ($property->exposes($col)) { + $id[$col] = $property->extract($object); + } + } + } + return $this->serializeRowId($id, true); + } + + function serializeRowId($identity, $check = false) { + if (is_scalar($identity)) { + return $identity; + } + + if ($check && !isset($identity)) { + return false; + } + + if (is_array($identity)) { + if ($check && array_search(null, $identity, true)) { + return false; + } + /* one level is better than no level */ + asort($identity); + } + return json_encode($identity); + } + + function hasObject($row_id) { + return isset($this->obj[$row_id]); + } + + function createObject(Row $row) { + $rid = $this->rowId($row); + $cls = $this->map->getClass(); + $obj = new $cls; + $oid = $this->objectId($obj); + $this->obj[$rid] = $obj; + $this->row[$oid] = $row; + return $obj; + } + + function resetObject(Row $row) { + unset($this->obj[$this->rowId($row)]); + } + + function getObject(Row $row) { + $id = $this->rowId($row); + return $this->getObjectById($id); + } + + function getObjectById($row_id) { + if (!$this->hasObject($row_id)) { + throw new BadMethodCallException("Object of row with id $row_id does not exist"); + } + return $this->obj[$row_id]; + } + + function asObject(Row $row){ + return $this->hasObject($this->rowId($row)) + ? $this->getObject($row) + : $this->createObject($row); + } + + function hasRow($obj_id) { + return isset($this->row[$obj_id]); + } + + function createRow($object) { + $oid = $this->objectId($object); + $row = new Row($this->map->getGateway()); + $this->row[$oid] = $row; + return $row; + } + + function resetRow($object) { + unset($this->row [$this->objectId($object)]); + } + + function getRow($object) { + $id = $this->objectId($object); + + if (!$this->hasRow($id)) { + throw new BadMethodCallException("Row for object with id $id does not exist"); + } + return $this->row[$id]; + } + + function asRow($object) { + return $this->hasRow($this->objectId($object)) + ? $this->getRow($object) + : $this->createRow($object); + } +} \ No newline at end of file diff --git a/lib/pq/Mapper/Property.php b/lib/pq/Mapper/Property.php new file mode 100644 index 0000000..ba4495b --- /dev/null +++ b/lib/pq/Mapper/Property.php @@ -0,0 +1,46 @@ +container = $container; + } + + function getContainer() { + return $this->container; + } + + function getProperty() { + return $this->property; + } + + function defines($property) { + return $this->property === $property; + } + + function exposes($field) { + return $this->field === $field; + } + + function assign($object, $value) { + $this->mapper + ->getReflector($object, $this->property) + ->setValue($object, $value); + } + + function extract($object) { + return $this->mapper + ->getReflector($object, $this->property) + ->getValue($object); + } + + function __toString() { + return sprintf("%s: %s(%s)", get_class($this), $this->property, $this->field?:"NULL"); + } +} \ No newline at end of file diff --git a/lib/pq/Mapper/Property/All.php b/lib/pq/Mapper/Property/All.php new file mode 100644 index 0000000..b53efdd --- /dev/null +++ b/lib/pq/Mapper/Property/All.php @@ -0,0 +1,95 @@ +mapper = $mapper; + $this->property = $property; + } + + function read(Row $row, $objectToUpdate) { + $val = $this->extract($objectToUpdate); + if (!isset($val)) { + $map = $this->mapper->mapOf($this->refClass); + $all = $map->allOf($row, $this->refName, $objects); + $this->assign($objectToUpdate, $objects); + $map->mapAll($all); + } + } + + function write($object, Row $rowToUpdate) { + $property = $this->findRefProperty($object); + $map = $this->mapper->mapOf($this->refClass); + $refs = $this->extract($object); + foreach ($refs as $ref) { + $property->assign($ref, $object); + } + return function() use($map, $refs) { + foreach ($refs as $ref) { + $map->unmap($ref); + } + }; + + if (!$this->container->getObjects()->rowId($rowToUpdate, true)) { + return [$this, "write"]; + } else { + /* $object = User */ + /* $refs = array(Email) */ + /* $property = Property\Ref(Email::$user)->to(User)->by("email_user") */ + /* now update array(Email) with id of User, i.e. $ref->user_id = $object->id */ + $map = $this->mapper->mapOf($this->refClass); + $refs = $this->extract($object); + foreach ($refs as $ref) { + $property->assign($ref, $object); + $map->unmap($ref); + } + } + } + + private function findRefProperty($object) { + $map = $this->mapper->mapOf($this->refClass); + $property = array_filter($map->getProperties(), function($property) use($object) { + if ($property instanceof RefPropertyInterface) { + return $property->references($object) && $property->on($this->refName); + } + }); + + if (1 != count($property)) { + // FIXME: move the decl + throw new UnexpectedValueException( + sprintf("%s does not reference %s exactly once through %s", + $this->refClass, $this->container->getClass(), $this->refName)); + } + return current($property); + } + + function read2(RowGateway $row) { + #echo __METHOD__." ".$this; + $ref = $this->getRefMap()->ref($row, $this->refName); + $value = $this->mapper->map($ref, $this->refClass); + return [$this->property => $value]; + } + + function write2($object) { + #echo __METHOD__." ".$this; + $value = $this->extract($object); + foreach ($value as $ref) { + $this->mapper->queue(function() use(&$object, &$ref) { + $map = $this->getRefMap()->getRefMapping($this->refName); + $map->assign($ref, $object); + $this->mapper->unmap($ref, $this->getRefMap()); + }); + } + return []; + } +} \ No newline at end of file diff --git a/lib/pq/Mapper/Property/Field.php b/lib/pq/Mapper/Property/Field.php new file mode 100644 index 0000000..7a57741 --- /dev/null +++ b/lib/pq/Mapper/Property/Field.php @@ -0,0 +1,31 @@ +mapper = $mapper; + $this->property = $property; + $this->field = $field ?: $property; + } + + function read(Row $row, $objectToUpdate) { + /* @var $val \pq\Gateway\Cell */ + $val = $row->{$this->field}; + $this->assign($objectToUpdate, $val->get()); + } + + function write($object, Row $rowToUpdate) { + $val = $this->extract($object); + $rowToUpdate->{$this->field} = $val; + } +} \ No newline at end of file diff --git a/lib/pq/Mapper/Property/Ref.php b/lib/pq/Mapper/Property/Ref.php new file mode 100644 index 0000000..851df41 --- /dev/null +++ b/lib/pq/Mapper/Property/Ref.php @@ -0,0 +1,75 @@ +mapper = $mapper; + $this->property = $property; + } + + function read(Row $row, $objectToUpdate) { + $val = $this->extract($objectToUpdate); + if (!isset($val)) { + $map = $this->mapper->mapOf($this->refClass); + $ref = $map->refOf($row, $this->refName, $objects)->current(); + $this->assign($objectToUpdate, current($objects)); + $map->map($ref); + } + } + + function write($object, Row $rowToUpdate) { + $map = $this->mapper->mapOf($this->refClass); + $ref = $this->extract($object); + $rel = $map->relOf($this->container, $this->refName); + foreach ($rel as $fgn => $col) { + foreach ($this->findFieldProperty($col) as $property) { + $value = $property->extract($ref); + $rowToUpdate->$fgn = $value; + } + } + } + + private function findFieldProperty($col) { + $map = $this->mapper->mapOf($this->refClass); + return array_filter($map->getProperties(), function($property) use($col) { + return $property->exposes($col); + }); + } + + + function read2(RowGateway $row) { + #echo __METHOD__." ".$this; + $map = $this->getRefMap(); + $rel = $this->container->getGateway()->getRelation( + $map->getGateway()->getName(), $this->refName); + $key = array_combine($rel->referencedColumns, array_map(function($c) use($row) { + return $row->$c->get(); + }, $rel->foreignColumns)); + if (($obj = $this->mapper->objectOfRowId($this->refClass, $key))) { + yield $this->property => $obj; + } else foreach ($map->getGateway()->by($row, $this->refName) as $row) { + yield $this->property => $this->mapper->objectOf($this->refClass, $row); + } + } + + function write2($object) { + #echo __METHOD__." ".$this; + $map = $this->getRefMap(); + $rel = $this->container->getGateway()->getRelation( + $map->getGateway()->getName(), $this->refName); + $ref = $this->extract($object); + foreach ($rel as $fgn => $col) { + $fld = $map->getFieldMapping($col); + yield $fgn => $fld->extract($ref); + } + } +} \ No newline at end of file diff --git a/lib/pq/Mapper/PropertyInterface.php b/lib/pq/Mapper/PropertyInterface.php new file mode 100644 index 0000000..da6ccd3 --- /dev/null +++ b/lib/pq/Mapper/PropertyInterface.php @@ -0,0 +1,23 @@ +refClass = $class; + return $this; + } + + function references($class) { + return $this->refClass === (is_object($class) ? get_class($class) : $class); + } + + function by($ref) { + $this->refName = $ref; + return $this; + } + + function on($ref) { + return $this->refName === $ref; + } + +} diff --git a/lib/pq/Mapper/RefPropertyInterface.php b/lib/pq/Mapper/RefPropertyInterface.php new file mode 100644 index 0000000..5fae13a --- /dev/null +++ b/lib/pq/Mapper/RefPropertyInterface.php @@ -0,0 +1,28 @@ +class = $class; + $this->mapper = $mapper; + $this->mapping = $mapper->mapOf($class); + $this->gateway = $this->mapping->getGateway(); + } + + function getMapper() { + return $this->mapper; + } + + function find($where = [], $order = null, $limit = null, $offset = null) { + /* @var pq\Gateway\Rowset $rowset */ + $rowset = $this->gateway->find($where, $order, $limit, $offset); + return $this->mapping->mapAll($rowset); + } + + function delete($object) { + $cache = $this->mapping->getObjects(); + $row = $cache->asRow($object)->delete(); + $cache->resetObject($row); + $cache->resetRow($object); + } + + function persist($object) { + $this->mapping->unmap($object); + } + +} \ No newline at end of file diff --git a/lib/pq/Mapper/StorageInterface.php b/lib/pq/Mapper/StorageInterface.php new file mode 100644 index 0000000..db16df5 --- /dev/null +++ b/lib/pq/Mapper/StorageInterface.php @@ -0,0 +1,15 @@ +