From b73d046e0ab04e92fbccc839ffa150863f9cb104 Mon Sep 17 00:00:00 2001 From: Dave Mc Nicoll <info@mcnd.ca> Date: Wed, 21 Aug 2019 16:13:00 -0400 Subject: [PATCH] - First draft of current WIP --- composer.json | 18 + src/Adapter/AdapterInterface.php | 10 + src/Adapter/MariaDB.php | 7 + src/Adapter/MySQL.php | 77 ++++ src/Annotation/Annotation.php | 5 + src/Annotation/AnnotationReader.php | 103 +++++ src/Annotation/Classes/Collation.php | 7 + src/Annotation/Classes/Method.php | 7 + src/Annotation/Classes/Table.php | 15 + src/Annotation/Property/Field.php | 29 ++ src/Annotation/Property/Field/CreatedAt.php | 13 + src/Annotation/Property/Field/ForeignKey.php | 19 + src/Annotation/Property/Field/Id.php | 15 + src/Annotation/Property/Field/UpdatedAt.php | 14 + src/Annotation/Property/GroupBy.php | 15 + src/Annotation/Property/OrderBy.php | 23 ++ src/Annotation/Property/Relation.php | 29 ++ src/Annotation/Property/Where.php | 15 + src/Common/ArrayObjectTrait.php | 212 ++++++++++ src/Common/EntityField.php | 56 +++ src/Common/EntityResolver.php | 185 +++++++++ src/Common/ObjectReflection.php | 273 +++++++++++++ src/Common/PdoObject.php | 45 +++ src/Common/Sql.php | 67 +++ src/ConnectionAdapter.php | 87 ++++ src/EntityCollection.php | 8 + src/EntityTrait.php | 82 ++++ src/Modeler/Field.php | 0 src/Modeler/Query.php | 404 +++++++++++++++++++ src/Modeler/Schema.php | 21 + src/Query/Explain.php | 17 + src/Query/Fragment.php | 15 + src/Query/From.php | 43 ++ src/Query/GroupBy.php | 29 ++ src/Query/Having.php | 7 + src/Query/Insert.php | 9 + src/Query/Join.php | 28 ++ src/Query/Like.php | 7 + src/Query/Limit.php | 23 ++ src/Query/MySQL/Replace.php | 17 + src/Query/Offset.php | 23 ++ src/Query/OrderBy.php | 34 ++ src/Query/Select.php | 44 ++ src/Query/Where.php | 151 +++++++ src/QueryBuilder.php | 182 +++++++++ src/Repository.php | 254 ++++++++++++ src/Ulmus.php | 53 +++ 47 files changed, 2797 insertions(+) create mode 100644 composer.json create mode 100644 src/Adapter/AdapterInterface.php create mode 100644 src/Adapter/MariaDB.php create mode 100644 src/Adapter/MySQL.php create mode 100644 src/Annotation/Annotation.php create mode 100644 src/Annotation/AnnotationReader.php create mode 100644 src/Annotation/Classes/Collation.php create mode 100644 src/Annotation/Classes/Method.php create mode 100644 src/Annotation/Classes/Table.php create mode 100644 src/Annotation/Property/Field.php create mode 100644 src/Annotation/Property/Field/CreatedAt.php create mode 100644 src/Annotation/Property/Field/ForeignKey.php create mode 100644 src/Annotation/Property/Field/Id.php create mode 100644 src/Annotation/Property/Field/UpdatedAt.php create mode 100644 src/Annotation/Property/GroupBy.php create mode 100644 src/Annotation/Property/OrderBy.php create mode 100644 src/Annotation/Property/Relation.php create mode 100644 src/Annotation/Property/Where.php create mode 100644 src/Common/ArrayObjectTrait.php create mode 100644 src/Common/EntityField.php create mode 100644 src/Common/EntityResolver.php create mode 100644 src/Common/ObjectReflection.php create mode 100644 src/Common/PdoObject.php create mode 100644 src/Common/Sql.php create mode 100644 src/ConnectionAdapter.php create mode 100644 src/EntityCollection.php create mode 100644 src/EntityTrait.php create mode 100644 src/Modeler/Field.php create mode 100644 src/Modeler/Query.php create mode 100644 src/Modeler/Schema.php create mode 100644 src/Query/Explain.php create mode 100644 src/Query/Fragment.php create mode 100644 src/Query/From.php create mode 100644 src/Query/GroupBy.php create mode 100644 src/Query/Having.php create mode 100644 src/Query/Insert.php create mode 100644 src/Query/Join.php create mode 100644 src/Query/Like.php create mode 100644 src/Query/Limit.php create mode 100644 src/Query/MySQL/Replace.php create mode 100644 src/Query/Offset.php create mode 100644 src/Query/OrderBy.php create mode 100644 src/Query/Select.php create mode 100644 src/Query/Where.php create mode 100644 src/QueryBuilder.php create mode 100644 src/Repository.php create mode 100644 src/Ulmus.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..61acd3e --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "mcnd/orm", + "description": "An hybrid of Active Record and Data Mapper pattern allowing fdirect queries.", + "type": "library", + "license": "MIT", + "authors": [ + { + "name": "Dave Mc Nicoll", + "email": "mcndave@gmail.com" + } + ], + "require": {}, + "autoload": { + "psr-4": { + "Ulmus\\": "src/" + } + } +} diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php new file mode 100644 index 0000000..06c40e0 --- /dev/null +++ b/src/Adapter/AdapterInterface.php @@ -0,0 +1,10 @@ +<?php + +namespace Ulmus\Adapter; + +use Ulmus\Common\PdoObject; + +interface AdapterInterface { + public function connect() : PdoObject; + public function buildDataSourceName() : string; +} diff --git a/src/Adapter/MariaDB.php b/src/Adapter/MariaDB.php new file mode 100644 index 0000000..5d8495b --- /dev/null +++ b/src/Adapter/MariaDB.php @@ -0,0 +1,7 @@ +<?php + +namespace Ulmus\Adapter; + +class MariaDB extends MySQL { + +} diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php new file mode 100644 index 0000000..4b0c5e0 --- /dev/null +++ b/src/Adapter/MySQL.php @@ -0,0 +1,77 @@ +<?php + +namespace Ulmus\Adapter; + +use Ulmus\Common\PdoObject; + +class MySQL implements AdapterInterface { + + public string $hostname; + public string $database; + public string $username; + public string $password; + public string $charset; + public int $port; + + public function __construct( + ?string $hostname = null, + ?string $database = null, + ?string $username = null, + ?string $password = null, + ?int $port = null, + ?string $charset = null + ) { + if ($hostname) { + $this->hostname = $hostname; + } + + if ($database) { + $this->database = $database; + } + + if ($port) { + $this->port = $port; + } + + if ($username) { + $this->username = $username; + } + + if ($password) { + $this->password = $password; + } + + if ($charset) { + $this->charset = $charset; + } + } + + public function connect() : PdoObject + { + try { + $obj = new PdoObject($this->buildDataSourceName(), $this->username, $this->password); + } + catch(PDOException $ex){ + throw $ex; + } + + return $obj; + } + + public function buildDataSourceName() : string + { + $parts[] = "host={$this->hostname}"; + $parts[] = "dbname={$this->database}"; + $parts[] = "port={$this->port}"; + + if ( $this->socket ?? false ) { + $parts[] = "socket={$this->socket}"; + } + + if ( $this->charset ?? false ) { + $parts[] = "charset={$this->charset}"; + } + + return "mysql:" . implode(';', $parts); + } +} diff --git a/src/Annotation/Annotation.php b/src/Annotation/Annotation.php new file mode 100644 index 0000000..ac55c70 --- /dev/null +++ b/src/Annotation/Annotation.php @@ -0,0 +1,5 @@ +<?php + +namespace Ulmus\Annotation; + +interface Annotation {} diff --git a/src/Annotation/AnnotationReader.php b/src/Annotation/AnnotationReader.php new file mode 100644 index 0000000..76abd00 --- /dev/null +++ b/src/Annotation/AnnotationReader.php @@ -0,0 +1,103 @@ +<?php + +namespace Ulmus\Annotation; + +use Reflector, ReflectionClass, ReflectionProperty, ReflectionMethod; + +/** + * This class exists while waiting for the official RFC [ https://wiki.php.net/rfc/annotations_v2 ] + */ +class AnnotationReader +{ + const PHP_TYPES = [ "string", "int", "float", "object", "double", "closure", ]; + + public string $class; + + public function __construct($class) { + $this->class = $class; + } + + public static function fromClass($class) : self + { + return new static($class); + } + + public function getProperty(ReflectionProperty $property) + { + return $this->parseDocComment($property); + } + + public function getClass(ReflectionClass $class) + { + return $this->parseDocComment($class); + } + + public function getMethod(ReflectionMethod $method) + { + return $this->parseDocComment($method); + } + + protected function parseDocComment(Reflector $reflect) + { + $namespace = $this->getObjectNamespace($reflect); + $tags = []; + + foreach(preg_split("/\r\n|\n|\r/", $reflect->getDocComment()) as $line) { + $line = ltrim($line, "* \t\/"); + $line = rtrim($line, "\t "); + + if ( substr($line, 0, 1) === '@' ) { + $line = ltrim($line, '@'); + + $open = strpos($line, "("); + $close = strrpos($line, ")"); + + if ( ! in_array(false, [ $open, $close ], true) && ( ++$open !== $close ) ) { + $arguments = substr($line, $open, $close - $open); + + try { + $tags[] = [ + 'tag' => substr($line, 0, $open - 1), + 'arguments' => eval("namespace $namespace; return [ $arguments ];"), + ]; + } + catch(\Throwable $error) { + throw new \InvalidArgumentException("An error occured while parsing annotation from '" . $this->getObjectName($reflect) . "' : @$line -- " . $error->getMessage()); + } + } + else { + $tags[] = [ + 'tag' => $line, + 'arguments' => [], + ]; + } + } + } + + return $tags; + } + + protected function getObjectName(Reflector $reflect) : string + { + switch(true) { + case $reflect instanceof ReflectionMethod : + case $reflect instanceof ReflectionProperty : + return $reflect->class . "::" . $reflect->name; + + case $reflect instanceof ReflectionClass : + return $reflect->name; + } + } + + protected function getObjectNamespace(Reflector $reflect) : string + { + switch(true) { + case $reflect instanceof ReflectionMethod : + case $reflect instanceof ReflectionProperty : + return $reflect->getDeclaringClass()->getNamespaceName(); + + case $reflect instanceof ReflectionClass : + return $reflect->getNamespaceName(); + } + } +} diff --git a/src/Annotation/Classes/Collation.php b/src/Annotation/Classes/Collation.php new file mode 100644 index 0000000..225c646 --- /dev/null +++ b/src/Annotation/Classes/Collation.php @@ -0,0 +1,7 @@ +<?php + +namespace Ulmus\Annotation\Classes; + +class Collation implements \Ulmus\Annotation\Annotation { + +} diff --git a/src/Annotation/Classes/Method.php b/src/Annotation/Classes/Method.php new file mode 100644 index 0000000..589aba6 --- /dev/null +++ b/src/Annotation/Classes/Method.php @@ -0,0 +1,7 @@ +<?php + +namespace Ulmus\Annotation\Classes; + +class Function implements \Ulmus\Annotation\Annotation { + +} diff --git a/src/Annotation/Classes/Table.php b/src/Annotation/Classes/Table.php new file mode 100644 index 0000000..2d10134 --- /dev/null +++ b/src/Annotation/Classes/Table.php @@ -0,0 +1,15 @@ +<?php + +namespace Ulmus\Annotation\Classes; + +class Table implements \Ulmus\Annotation\Annotation { + + public string $name; + + public function __construct($name = null) + { + if ( $name !== null ) { + $this->name = $name; + } + } +} diff --git a/src/Annotation/Property/Field.php b/src/Annotation/Property/Field.php new file mode 100644 index 0000000..c6671a5 --- /dev/null +++ b/src/Annotation/Property/Field.php @@ -0,0 +1,29 @@ +<?php + +namespace Ulmus\Annotation\Property; + +class Field implements \Ulmus\Annotation\Annotation { + + public string $type; + + public string $name; + + public int $length; + + public array $attributes = []; + + public bool $nullable = false; + + public function __construct(string $type = null, int $length = null) + { + switch(true) { + case $type !== null: + $this->type = $type; + break; + + case $length !== null: + $this->length = $length; + break; + } + } +} diff --git a/src/Annotation/Property/Field/CreatedAt.php b/src/Annotation/Property/Field/CreatedAt.php new file mode 100644 index 0000000..61b831f --- /dev/null +++ b/src/Annotation/Property/Field/CreatedAt.php @@ -0,0 +1,13 @@ +<?php + +namespace Ulmus\Annotation\Property\Field; + +class CreatedAt extends \Ulmus\Annotation\Property\Field { + + public function __construct() + { + $this->nullable = false; + $this->type = "timestamp"; + $this->attributes['default'] = "CURRENT_TIMESTAMP"; + } +} diff --git a/src/Annotation/Property/Field/ForeignKey.php b/src/Annotation/Property/Field/ForeignKey.php new file mode 100644 index 0000000..f0ae7d0 --- /dev/null +++ b/src/Annotation/Property/Field/ForeignKey.php @@ -0,0 +1,19 @@ +<?php + +namespace Ulmus\Annotation\Property\Field; + +/** + * Since we need consistancy between the declaration of our ID and FK fields, it + * was decided to extend the Id class instead of Field for this case. + */ +class ForeignKey extends Id { + + public function __construct() + { + parent::__construct(); + + unset($this->nullable); + $this->attributes['primary_key'] = false; + } + +} diff --git a/src/Annotation/Property/Field/Id.php b/src/Annotation/Property/Field/Id.php new file mode 100644 index 0000000..e7f51d2 --- /dev/null +++ b/src/Annotation/Property/Field/Id.php @@ -0,0 +1,15 @@ +<?php + +namespace Ulmus\Annotation\Property\Field; + +class Id extends \Ulmus\Annotation\Property\Field { + + public function __construct() + { + $this->nullable = false; + $this->type = "int"; + $this->attributes['unsigned'] = true; + $this->attributes['primary_key'] = true; + } + +} diff --git a/src/Annotation/Property/Field/UpdatedAt.php b/src/Annotation/Property/Field/UpdatedAt.php new file mode 100644 index 0000000..5a33dce --- /dev/null +++ b/src/Annotation/Property/Field/UpdatedAt.php @@ -0,0 +1,14 @@ +<?php + +namespace Ulmus\Annotation\Property\Field; + +class UpdatedAt extends \Ulmus\Annotation\Property\Field { + + public function __construct() + { + $this->nullable = true; + $this->type = "timestamp"; + $this->attributes['update'] = "CURRENT_TIMESTAMP"; + $this->attributes['default'] = null; + } +} diff --git a/src/Annotation/Property/GroupBy.php b/src/Annotation/Property/GroupBy.php new file mode 100644 index 0000000..6d3eca9 --- /dev/null +++ b/src/Annotation/Property/GroupBy.php @@ -0,0 +1,15 @@ +<?php + +namespace Ulmus\Annotation\Property; + +class GroupBy implements \Ulmus\Annotation\Annotation { + + public array $fields = []; + + public function __construct(...$field) + { + if ( $field ) { + $this->fields = $field; + } + } +} diff --git a/src/Annotation/Property/OrderBy.php b/src/Annotation/Property/OrderBy.php new file mode 100644 index 0000000..0bf9175 --- /dev/null +++ b/src/Annotation/Property/OrderBy.php @@ -0,0 +1,23 @@ +<?php + +namespace Ulmus\Annotation\Property; + +class OrderBy implements \Ulmus\Annotation\Annotation { + + public string $field; + + public string $order = "ASC"; + + public function __construct(string $field = null, string $order = null) + { + switch(true) { + case $field !== null: + $this->field = $field; + break; + + case $order !== null: + $this->order = $order; + break; + } + } +} diff --git a/src/Annotation/Property/Relation.php b/src/Annotation/Property/Relation.php new file mode 100644 index 0000000..634d8e5 --- /dev/null +++ b/src/Annotation/Property/Relation.php @@ -0,0 +1,29 @@ +<?php + +namespace Ulmus\Annotation\Property; + +class Relation implements \Ulmus\Annotation\Annotation { + + public string $type; + + public string $key; + + public string $foreignKey; + + public array $foreignKeys; + + public string $bridge; + + public string $bridgeKey; + + public string $bridgeForeignKey; + + public function __construct(string $type = null) + { + switch(true) { + case $type !== null: + $this->type = $type; + break; + } + } +} diff --git a/src/Annotation/Property/Where.php b/src/Annotation/Property/Where.php new file mode 100644 index 0000000..27a457f --- /dev/null +++ b/src/Annotation/Property/Where.php @@ -0,0 +1,15 @@ +<?php + +namespace Ulmus\Annotation\Property; + +class Where implements \Ulmus\Annotation\Annotation { + + public array $comparisons = []; + + public function __construct(...$comparisons) + { + if ( $comparisons ) { + $this->comparisons = $comparisons; + } + } +} diff --git a/src/Common/ArrayObjectTrait.php b/src/Common/ArrayObjectTrait.php new file mode 100644 index 0000000..172df34 --- /dev/null +++ b/src/Common/ArrayObjectTrait.php @@ -0,0 +1,212 @@ +<?php + +namespace Ulmus\Common; + +trait ArrayObjectTrait { + + protected $arrayobject_pointer = null; + + protected $arrayobject_container = []; + + protected $arrayobject_changed = []; + + protected $arrayobject_selected = false; + + public function count() : int + { + return count( $this->arrayobject_container() ); + } + + public function contains($term, $strict = false) : bool + { + return (array_search($term, $this->arrayobject_container(), $strict) !== false) ; + } + + public function &arrayobject_current() + { + if ( !is_null($this->arrayobject_pointer) ) { + $var = &$this->arrayobject_container()[$this->arrayobject_pointer] ?: []; + $var || ( $var = [] ); + return $var; + } + + if ( $this->arrayobject_selected !== false ){ + $ret = &$this->arrayobject_selected ?: []; + return $ret; + } + + # Restoring integrity of container since it could be nullified + if ( ! is_array($this->arrayobject_container()) ) { + $this->arrayobject_container([]); + } + + return $this->arrayobject_container(); + } + + public function offsetSet($offset, $value, $changed = null) + { + if ( $changed && (!isset($this->arrayobject_current()[$offset]) || ($this->arrayobject_current()[$offset] !== $value) ) ) { + $this->arrayobject_changed($offset, true); + } + + return is_null($offset) ? $this->arrayobject_current()[] = $value : $this->arrayobject_current()[$offset] = $value; + } + + public function arrayobject_set_pointer($pointer) + { + # $pointer could nullify obj pointer + if ( $this->arrayobject_pointer = $pointer ) { + # Creating dataset whenever we have a new one + if ( ! isset($this->arrayobject_container()[$this->arrayobject_pointer]) ) { + $this->arrayobject_container()[$this->arrayobject_pointer] = []; + $this->arrayobject_changed[$this->arrayobject_pointer] = []; + } + } + + return $this; + } + + public function arrayobject_select($selection, $purge = true) + { + if ( is_bool($selection) ) { + return $this->arrayobject_selected = $selection; + } + + $purge && ( $this->arrayobject_selected = [] ); + + foreach($selection as $pointer) { + $this->arrayobject_selected[$pointer] = &$this->arrayobject_container[$pointer]; + } + + return true; + } + + public function arrayobject_exist($pointer) : bool + { + return isset( $this->arrayobject_container()[$pointer] ); + } + + public function arrayobject_flush_changed() { + ! is_null($this->arrayobject_pointer) ? + $this->arrayobject_changed[$this->arrayobject_pointer] = [] + : + $this->arrayobject_changed = [] + ; + } + + public function arrayobject_changed($offset = null, $set = null) { + if ($offset) { + if ($set !== null) { + ! is_null($this->arrayobject_pointer) ? + ( $this->arrayobject_changed[$this->arrayobject_pointer][$offset] = $set ) + : + ( $this->arrayobject_changed[$offset] = $set ); + } + + return !is_null($this->arrayobject_pointer) ? $this->arrayobject_changed[$this->arrayobject_pointer][$offset] : $this->arrayobject_changed[$offset]; + } + + return array_keys( !is_null($this->arrayobject_pointer) + ? $this->arrayobject_changed[$this->arrayobject_pointer] ?? [] + : $this->arrayobject_changed) ?? []; + } + + public function arrayobject_remove($pointer) { + if ( isset($this->arrayobject_container()[$pointer]) ) { + unset( $this->arrayobject_container()[$pointer], $this->arrayobject_changed[$pointer]); + } + } + + public function arrayobject_iterate($callback) { + if ( $callback && is_callable($callback) ) { + $pointer = $this->arrayobject_pointer; + + foreach($this->arrayobject_container() as $key => $value) { + $this->arrayobject_set_pointer($key); + $callback($key, $this); + } + + $this->arrayobject_set_pointer($pointer); + } + + return $this; + } + + + public function offsetGet($offset) + { + if ( !is_null($this->arrayobject_pointer) ) { + return isset($this->arrayobject_container()[$this->arrayobject_pointer][$offset]) ? $this->arrayobject_container()[$this->arrayobject_pointer][$offset] : null; + } + else { + return isset($this->arrayobject_container()[$offset]) ? $this->arrayobject_container()[$offset] : null; + } + } + + public function offsetExists($offset) : bool + { + return array_key_exists($offset, $this->arrayobject_current() ); + } + + public function offsetUnset($offset) + { + if ( !is_null($this->arrayobject_pointer)) { + unset($this->arrayobject_container()[$this->arrayobject_pointer][$offset]); + } + else { + unset($this->arrayobject_container()[$offset]) ; + } + } + + public function arrayobject_sort($field, $order = 'ASC') + { + Arrayobj::order_by($this->arrayobject_container(), $field); + $order === 'DESC' && array_reverse($this->arrayobject_current()); + } + + public function rewind() + { + reset( $this->arrayobject_container() ); + + # Rewinding will also reset the pointer + $this->arrayobject_set_pointer(key($this->arrayobject_container())); + + return $this; + } + + public function current() + { + return $this->arrayobject_set_pointer( $this->key() ); + } + + public function key() + { + $var = key( $this->arrayobject_container() ); + return $var; + } + + public function next() + { + $var = next( $this->arrayobject_container() ); + return $var; + } + + public function valid() : bool + { + $key = $this->key(); + return ( $key !== NULL ) && ( $key !== FALSE ); + } + + protected function &arrayobject_container($set = null) + { + if ( $set !== null ) { + $this->arrayobject_container = $set; + } + + if ( $this->arrayobject_selected !== false ) { + return $this->arrayobject_selected; + } + + return $this->arrayobject_container; + } +} diff --git a/src/Common/EntityField.php b/src/Common/EntityField.php new file mode 100644 index 0000000..a15df85 --- /dev/null +++ b/src/Common/EntityField.php @@ -0,0 +1,56 @@ +<?php + +namespace Ulmus\Common; + +use Ulmus\Ulmus; + +class EntityField +{ + public string $name; + + public string $entityClass; + + public string $alias; + + protected EntityResolver $entityResolver; + + public function __construct(string $entityClass, string $name, string $alias) + { + $this->entityClass = $entityClass; + $this->name = $name; + $this->alias = $alias; + $this->entityResolver = Ulmus::resolveEntity(static::class); + } + + public function name($useAlias = true) : string + { + # Must use REFLECTION before throwing this value. + # Should first check if it's a relation field, and if it is, + # it's real key must be returned (PK usually) + return $useAlias ? "{$this->alias}.`{$this->name}`" : $this->name; + } + + public static function isScalarType($type) : bool + { + switch($type) { + case 'int': + case 'bool': + case 'string': + case 'float': + case 'double': + return true; + } + + return false; + } + + public static function isObjectType($type) : bool + { + return strpos($type, "\\") !== false; + } + + public function __toString() : string + { + return $this->name(); + } +} diff --git a/src/Common/EntityResolver.php b/src/Common/EntityResolver.php new file mode 100644 index 0000000..ae45936 --- /dev/null +++ b/src/Common/EntityResolver.php @@ -0,0 +1,185 @@ +<?php + +namespace Ulmus\Common; + +use Ulmus\Annotation\Annotation, + Ulmus\Annotation\Classes\Table, + Ulmus\Annotation\Property\Field; + +class EntityResolver { + + const KEY_ENTITY_NAME = 01; + const KEY_COLUMN_NAME = 02; + + public string $entityClass; + + public array $uses; + + public array $class; + + public array $properties; + + public array $methods; + + public function __construct(string $entityClass) + { + $this->entityClass = $entityClass; + + list($this->uses, $this->class, $this->methods, $this->properties) = array_values( + ObjectReflection::fromClass($entityClass)->read() + ); + + $this->resolveAnnotations(); + } + + public function field($name, $fieldKey = self::KEY_ENTITY_NAME) : ? array + { + try{ + return $this->fieldList($fieldKey)[$name]; + } + catch(\Throwable $e) { + throw new \InvalidArgumentException("Can't find entity field's column named `$name` from entity {$this->entityClass}"); + } + } + + public function fieldList($fieldKey = self::KEY_ENTITY_NAME) : array + { + $fieldList = []; + + foreach($this->properties as $item) { + foreach($item['tags'] ?? [] as $tag) { + if ( $tag['object'] instanceof Field ) { + switch($fieldKey) { + case static::KEY_ENTITY_NAME: + $key = $item['name']; + break; + + case static::KEY_COLUMN_NAME: + $key = $tag['object']->name ?? $item['name']; + break; + + default: + throw new \InvalidArgumentException("Given `fieldKey` is unknown to the EntityResolver"); + } + + $fieldList[$key] = $item; + break; + } + } + } + + return $fieldList; + } + + public function tableName() : string + { + if ( null === $table = $this->getAnnotationFromClassname( Table::class ) ) { + throw new \LogicException("Your entity {$this->entityClass} seems to be missing a @Table() annotation"); + } + + if ( $table->name === "" ) { + throw new \ArgumentCountError("Your entity {$this->entityClass} seems to be missing a `name` argument for your @Table() annotation"); + } + + return $table->name; + } + + public function primaryKeys() : array + { + + } + + /** + * Transform an annotation into it's object's counterpart + */ + public function getAnnotationFromClassname(string $className) : ? object + { + if ( $name = $this->uses[$className] ?? false) { + + foreach($this->class['tags'] as $item) { + if ( $item['tag'] === $name ) { + return $this->instanciateAnnotationObject($item); + } + + foreach($this->properties as $item) { + foreach($item['tags'] as $item) { + if ( $item['tag'] === $name ) { + return $this->instanciateAnnotationObject($item); + } + } + } + + foreach($this->methods as $item) { + foreach($item['tags'] as $item) { + if ( $item['tag'] === $name ) { + return $this->instanciateAnnotationObject($item); + } + } + } + + } + + throw new \TypeError("Annotation `$className` could not be found within your object `{$this->entityClass}`"); + } + else { + throw new \InvalidArgumentException("Class `$className` was not found within {$this->entityClass} uses statement (or it's children / traits)"); + } + + return null; + } + + public function instanciateAnnotationObject(array $tagDefinition) : Annotation + { + $arguments = $this->extractArguments($tagDefinition['arguments']); + + if ( false === $class = array_search($tagDefinition['tag'], $this->uses) ) { + throw new \InvalidArgumentException("Annotation class `{$tagDefinition['tag']}` was not found within {$this->entityClass} uses statement (or it's children / traits)"); + } + + $obj = new $class(... $arguments['constructor']); + + foreach($arguments['setter'] as $key => $value) { + $obj->$key = $value; + } + + return $obj; + } + + /** + * Extracts arguments from an Annotation definition, easing object's declaration. + */ + protected function extractArguments(array $arguments) : array + { + $list = [ + 'setter' => [], + 'constructor' => [], + ]; + + ksort($arguments); + + foreach($arguments as $key => $value) { + $list[ is_int($key) ? 'constructor' : 'setter' ][$key] = $value; + } + + return $list; + } + + protected function resolveAnnotations() + { + foreach($this->class['tags'] as &$tag) { + $tag['object'] = $this->instanciateAnnotationObject($tag); + } + + foreach($this->properties as &$property) { + foreach($property['tags'] as &$tag){ + $tag['object'] = $this->instanciateAnnotationObject($tag); + } + } + + foreach($this->methods as &$method) { + foreach($method['tags'] as &$tag){ + $tag['object'] = $this->instanciateAnnotationObject($tag); + } + } + } +} diff --git a/src/Common/ObjectReflection.php b/src/Common/ObjectReflection.php new file mode 100644 index 0000000..77de42d --- /dev/null +++ b/src/Common/ObjectReflection.php @@ -0,0 +1,273 @@ +<?php + +namespace Ulmus\Common; + +use Ulmus\Ulmus; + +use Ulmus\Annotation\AnnotationReader; + +use Reflector, ReflectionClass, ReflectionProperty, ReflectionMethod; + +class ObjectReflection { + + public AnnotationReader $annotationReader; + + public function __construct($class, AnnotationReader $annotationReader = null) { + $this->classReflection = $class instanceof ReflectionClass ? $class : new ReflectionClass($class); + $this->annotationReader = $annotationReader ?: AnnotationReader::fromClass($class); + } + + public static function fromClass($class) : self + { + return new static($class); + } + + public function read() : array + { + return [ + 'uses' => $this->gatherUses(true), + 'class' => $this->gatherClass(true), + 'method' => $this->gatherMethods(true), + 'property' => $this->gatherProperties(true), + ]; + } + + public function gatherUses(bool $full = true) : array + { + $list = []; + + if ( $full ) { + if ( $parentClass = $this->classReflection->getParentClass() ) { + $list = static::fromClass($parentClass)->gatherUses(true); + } + + foreach($this->classReflection->getTraits() as $trait) { + $list = array_merge($list, static::fromClass($trait)->gatherUses(true)); + } + } + + return array_merge($this->getUsesStatements(), $list); + } + + public function gatherClass(bool $full = true) : array + { + $class = []; + + if ( $full ) { + if ( $parentClass = $this->classReflection->getParentClass() ) { + $class = static::fromClass($parentClass)->gatherClass(true); + } + + $itemName = function($item) { + return $item->getName(); + }; + } + + return [ + 'tags' => array_merge($class, $this->annotationReader->getClass($this->classReflection)) + ] + ( ! $full ? [] : [ + 'traits' => array_map($itemName, $this->classReflection->getTraits()), + 'interfaces' => array_map($itemName, $this->classReflection->getInterfaces()), + ]); + } + + public function gatherProperties(bool $full = true, int $filter = + ReflectionProperty::IS_PUBLIC | + ReflectionProperty::IS_PROTECTED | + ReflectionProperty::IS_PRIVATE + ) : array + { + $properties = []; + $defaultValues = $this->classReflection->getDefaultProperties(); + + if ( $full ) { + if ( $parentClass = $this->classReflection->getParentClass() ) { + $properties = static::fromClass($parentClass)->gatherProperties($full, $filter); + } + } + + $properties = array_merge($properties, $this->classReflection->getProperties($filter)); + + $list = []; + + foreach($properties as $property) { + $current = [ + 'name' => $property->getName() + ]; + + # Default value can be 'null', so isset() it not suitable here + if ( array_key_exists($current['name'], $defaultValues) ) { + $current['value'] = $defaultValues[ $current['name'] ]; + } + + if ( $property->hasType() ) { + $current['type'] = $property->getType()->getName(); + $current['nullable'] = $property->getType()->allowsNull(); + } + + $current['tags'] = $this->annotationReader->getProperty($property); + + if ( $this->ignoreElementAnnotation($current['tags']) ) { + continue; + } + + $list[ $current['name'] ] = $current; + } + + return $list; + } + + public function gatherMethods(bool $full = true, int $filter = + ReflectionMethod::IS_PUBLIC | + ReflectionMethod::IS_PROTECTED | + ReflectionMethod::IS_PRIVATE | + ReflectionMethod::IS_STATIC + ) : array + { + $methods = []; + + if ( $full ) { + if ( $parentClass = $this->classReflection->getParentClass() ) { + $methods = static::fromClass($parentClass)->gatherMethods($full, $filter); + } + } + + $methods = array_merge($methods, $this->classReflection->getMethods($filter)); + + $list = []; + + foreach($methods as $method) { + $parameters = []; + + foreach($method->getParameters() as $parameter) { + $parameters[$parameter->getName()] = [ + 'null' => $parameter->allowsNull(), + 'position' => $parameter->getPosition(), + 'type' => $parameter->hasType() ? $parameter->getType()->getName() : false, + 'array' => $parameter->isArray(), + 'callable' => $parameter->isCallable(), + 'optional' => $parameter->isOptional(), + 'byReference' => $parameter->isPassedByReference(), + ]; + } + + $current = [ + 'name' => $method->getName(), + 'type' => $method->hasReturnType() ? $method->getReturnType()->getName() : false, + 'constructor' => $method->isConstructor(), + 'destructor' => $method->isDestructor(), + 'parameters' => $parameters, + ]; + + $current['tags'] = $this->annotationReader->getMethod($method); + + if ( $this->ignoreElementAnnotation($current['tags']) ) { + continue; + } + + $list[ $current['name'] ] = $current; + } + + return $list; + } + + protected function ignoreElementAnnotation($tags) : bool + { + return in_array('IGNORE', array_map('strtoupper', array_column($tags, 'tag') )); + } + + + protected function readCode() : string + { + static $code = []; + $fileName = $this->classReflection->getFilename(); + return $code[$fileName] ?? $code[$fileName] = file_get_contents($fileName); + } + + protected function getUsesStatements() : array + { + $uses = []; + $tokens = token_get_all( $c = $this->readCode() ); + + while ( $token = array_shift($tokens) ) { + + if ( is_array($token) ) { + list($token, $value) = $token; + } + + switch ($token) { + case T_CLASS: + case T_TRAIT: + case T_INTERFACE: + break 2; + + case T_USE: + $isUse = true; + break; + + case T_NS_SEPARATOR: + $isNamespace = $isUse; + break; + + case T_STRING: + if ( $isNamespace && $latestString ) { + $statement[] = $latestString; + } + + $latestString = $value; + break; + + case T_AS: + # My\Name\Space\aClassHere `as` ClassAlias; + $replacedClass = implode("\\", array_merge($statement, [ $latestString ])); + $latestString = null; + break; + + case T_WHITESPACE: + case T_COMMENT: + case T_DOC_COMMENT: + break; + + case '{': + # opening a sub-namespace -> \My\Name\Space\`{`OneItem, AnotherItem} + if ( $isNamespace ) { + $inNamespace = true; + } + break; + + case ';'; + case ',': + case '}': + if ( $isUse ) { + if ( $replacedClass ) { + $uses[$replacedClass] = $latestString; + $replacedClass = ""; + } + elseif ( $latestString ) { + $uses[implode("\\", array_merge($statement, [ $latestString ]))] = $latestString; + } + } + + if ( $inNamespace ) { + $latestString = ""; + + # \My\Name\Space\{OneItem, AnotherItem`}` <- closing a sub-namespace + if ( $token !== "}" ) { + break; + } + } + + case T_OPEN_TAG: + default: + $statement = []; + $latestString = ""; + $replacedClass = null; + $isNamespace = $inNamespace = false; + $isUse = ( $isUse ?? false ) && ( $token === ',' ); + break; + } + } + + return $uses; + } +} diff --git a/src/Common/PdoObject.php b/src/Common/PdoObject.php new file mode 100644 index 0000000..cc1274c --- /dev/null +++ b/src/Common/PdoObject.php @@ -0,0 +1,45 @@ +<?php + +namespace Ulmus\Common; + +use PDO, PDOStatement; + +class PdoObject extends PDO { + + public function select(string $sql, array $parameters = []) : PDOStatement + { + try { + if ( $statement = $this->prepare($sql) ) { + $statement = $this->execute($statement, $parameters, true); + $statement->setFetchMode(\PDO::FETCH_ASSOC); + return $statement; + } + } catch (\PDOException $e) { throw $e; } + } + + public function execute(PDOStatement $statement, array $parameters = [], bool $commit = true) : ? PDOStatement + { + try { + if (! $this->inTransaction() ) { + $this->beginTransaction(); + } + + if ( empty($parameters) ? $statement->execute() : $statement->execute($parameters) ) { + # if ( $commit ) { + $this->commit(); + # } + + return $statement; + } + else { + throw new PDOException('Could not begin transaction or given statement is invalid.'); + } + } catch (\PDOException $e) { + $this->rollback(); + throw $e; + } + + return null; + } + +} diff --git a/src/Common/Sql.php b/src/Common/Sql.php new file mode 100644 index 0000000..ce79fa4 --- /dev/null +++ b/src/Common/Sql.php @@ -0,0 +1,67 @@ +<?php + +namespace Ulmus\Common; + +abstract class Sql { + + public static function function($name, ...$arguments) + { + return new class($name, ...$arguments) { + + protected string $as = ""; + + protected string $name; + + protected array $arguments; + + public function __construct(string $name, ...$arguments) { + $this->name = $name; + $this->arguments = $arguments; + $this->parseArguments(); + } + + public function __toString() { + return implode(' ', array_filter([ + "{$this->name}(" . implode(", ", $this->arguments) . ")", + $this->as ? "AS {$this->as}" : false, + ])); + } + + public function as($fieldName) { + $this->as = $fieldName; + return $this; + } + + protected function parseArguments() { + foreach($this->arguments as &$item) { + $item = Sql::escape($item); + } + } + }; + } + + public static function escape($value) + { + switch(true) { + case is_object($value): + # @TODO Make sure the object is a Field + return (string) $value; + break; + + case is_string($value): + $value = "\"$value\""; + break; + + case is_null($value): + $value = "NULL"; + break; + } + + return $value; + } + + public static function parameter($value) : string + { + + } +} diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php new file mode 100644 index 0000000..cc4fe67 --- /dev/null +++ b/src/ConnectionAdapter.php @@ -0,0 +1,87 @@ +<?php + +namespace Ulmus; + +use Ulmus\Adapter\AdapterInterface; + +use Ulmus\Common\PdoObject; + +class ConnectionAdapter +{ + public string $name; + + public array $configuration; + + protected AdapterInterface $adapter; + + public PdoObject $pdo; + + public function __construct(string $name = "default", array $configuration = []) + { + $this->name = $name; + $this->configuration = $configuration; + + if ( $name === "default" ) { + Ulmus::$defaultAdapter = $this; + } + } + + public function resolveConfiguration() + { + $connection = $this->configuration['connections'][$this->name] ?? []; + + if ( $adapterName = $connection['adapter'] ?? false ) { + $this->adapter = $this->instanciateAdapter($adapterName); + } + else { + throw new \InvalidArgumentException("Adapter not found within your configuration array."); + } + + if ( false === $this->adapter->hostname = $connection['host'] ?? false ) { + throw new \InvalidArgumentException("Your `host` name is missing from your configuration array"); + } + + if ( false === $this->adapter->port = $connection['port'] ?? false ) { + throw new \InvalidArgumentException("Your `port` number is missing from your configuration array"); + } + + if ( false === $this->adapter->database = $connection['database'] ?? false ) { + throw new \InvalidArgumentException("Your `database` name is missing from your configuration array"); + } + + if ( false === $this->adapter->username = $connection['username'] ?? false ) { + throw new \InvalidArgumentException("Your `username` is missing from your configuration array"); + } + + if ( false === $this->adapter->password = $connection['password'] ?? false ) { + throw new \InvalidArgumentException("Your `password` is missing from your configuration array"); + } + } + + /** + * Connect the adapter + * @return self + */ + public function connect() + { + $this->pdo = $this->adapter->connect(); + $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); + $this->pdo->setAttribute(\PDO::ATTR_AUTOCOMMIT, false); + $this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); + $this->pdo->setAttribute(\PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false); + + return $this; + } + + /** + * Instanciate an adapter which interact with the data source + * @param string $name An Ulmus adapter or full class name implementing AdapterInterface + * @return AdapterInterface + */ + protected function instanciateAdapter($name) + { + $class = substr($name, 0, 2) === "\\" ? $name : "\\Ulmus\\Adapter\\$name"; + return new $class(); + } +} diff --git a/src/EntityCollection.php b/src/EntityCollection.php new file mode 100644 index 0000000..5b6b6e6 --- /dev/null +++ b/src/EntityCollection.php @@ -0,0 +1,8 @@ +<?php + +namespace Ulmus; + +class EntityCollection extends \ArrayObject +{ + use Common\ArrayObjectTrait; +} diff --git a/src/EntityTrait.php b/src/EntityTrait.php new file mode 100644 index 0000000..2b0d33b --- /dev/null +++ b/src/EntityTrait.php @@ -0,0 +1,82 @@ +<?php + +namespace Ulmus; + +use Ulmus\Repository, + Ulmus\Common\EntityResolver, + Ulmus\Common\EntityField; + +use Ulmus\Annotation\Classes\{ Method, Table, Collation as Test, }; +use Ulmus\Annotation\Property\{ Field, Relation, OrderBy, Where, }; +use Ulmus\Annotation\Property\Field\{ Id, ForeignKey, CreatedAt, UpdatedAt, }; + +trait EntityTrait { + + public function entityFillFromDataset($dataset) : self + { + $fields = Ulmus::resolveEntity(static::class); + + foreach($dataset as $key => $value) { + if ( null === $field = $fields->field($key, EntityResolver::KEY_COLUMN_NAME) ?? null ) { + throw new \Exception("Field `$key` can not be found within your entity ".static::class); + } + + if ( is_null($value) ) { + $this->{$field['name']} = null; + } + elseif ( $field['type'] === 'array' ) { + $this->{$field['name']} = substr($value, 0, 1) === "a" ? unserialize($value) : json_decode($value, true); + } + elseif ( EntityField::isScalarType($field['type']) ) { + $this->{$field['name']} = $value; + } + elseif ( EntityField::isObjectType($field['type']) ) { + $this->{$field['name']} = new $field['type'](); + } + } + + return $this; + } + + /** + * @Ignore + */ + public static function repository() : Repository + { + return Ulmus::repository(static::class); + } + + /** + * @Ignore + */ + public static function queryBuilder() : QueryBuilder + { + return Ulmus::queryBuilder(static::class); + } + + /** + * @Ignore + */ + public static function field($name, ? string $alias = null) + { + return new EntityField(static::class, $name, $alias ?: Repository::DEFAULT_ALIAS); + } + + /** + * @Ignore + */ + public static function fields(...$fields) + { + return implode(', ', array_map(function($name) { + return static::field($name); + }, $fields)); + } + + /** + * @Ignore + */ + public static function table() + { + return "REFLECT TABLE"; + } +} diff --git a/src/Modeler/Field.php b/src/Modeler/Field.php new file mode 100644 index 0000000..e69de29 diff --git a/src/Modeler/Query.php b/src/Modeler/Query.php new file mode 100644 index 0000000..0db7442 --- /dev/null +++ b/src/Modeler/Query.php @@ -0,0 +1,404 @@ +<?php namespace Alive\Storage\Sql; + +use Alive\{ + constructor, + Arrayobj +}; + +class QueryBuilder { + protected $fields; + protected $where; + protected $order_by; + protected $group_by; + protected $limit; + + public static $syntax = [ + 'against' => 'AGAINST', + 'and' => 'AND', + 'as' => 'AS', + 'charset' => 'CHARACTER SET', + 'collate' => 'COLLATE', + 'create' => 'CREATE', + 'database' => 'DATABASE', + 'delete' => 'DELETE FROM', + 'distinct' => 'DISTINCT', + 'drop' => 'DROP', + 'engine' => 'ENGINE', + '!exist' => 'IF NOT EXISTS', + 'exist' => 'IF EXISTS', + 'explain' => 'EXPLAIN', + 'from' => 'FROM', + 'grant' => 'GRANT', + 'grant_option' => 'GRANT OPTION', + 'group_by' => 'GROUP BY', + 'having' => 'HAVING', + 'in' => 'IN', + 'insert' => 'INSERT INTO', + 'join' => 'JOIN', + 'join-left' => 'LEFT', + 'join-right' => 'RIGHT', + 'join-inner' => 'INNER', + 'join-full' => 'FULL', + 'join-self' => 'SELF', + #'join-outer' => 'OUTER', + 'join-cross' => 'CROSS', + 'like' => 'LIKE', + 'limit' => 'LIMIT', + 'match' => 'MATCH', + 'not_in' => 'NOT IN', + 'on' => 'ON', + 'on_table' => 'ON TABLE', + 'or' => 'OR', + 'order_by' => 'ORDER BY', + 'offset' => 'OFFSET', + 'revoke' => 'REVOKE', + 'select' => 'SELECT', + 'set' => 'SET', + 'table' => 'TABLE', + 'table_charset' => 'DEFAULT CHARSET', + 'to' => 'TO', + 'update' => 'UPDATE', + 'values' => 'VALUES', + 'where' => 'WHERE' + ]; + + static $escape_char = '`'; + + protected $compiled = []; + + public static function select($param) { + $param = Arrayobj::make($param); + + return static::prepare_array([ + $param->if_has('explain' , static::$syntax['explain']), + static::$syntax['select'], + $param->if_has('distinct' , static::$syntax['distinct']), + static::group_fields($param['fields'] ?: '*'), + static::$syntax['from'], + static::full_tablename($param), + static::prepare_join($param['join']), + static::prepare_where($param['where'], false, $param->ternary('escaped', true)), + $param->if_has('group_by' , static::prepare_group($param['group_by'], $param['alias'] ?? null)), + $param->if_has('having' , static::$syntax['having']." {$param['having']}"), + /* @todo UNION | INTERSECT | EXCEPT GOES HERE !*/ + $param->if_has('order_by' , static::prepare_order($param['order_by'], $param['alias'] ?? null)), + static::prepare_limit($param) + ]); + } + + /** + * This function will translate parameters into a "create database" or "create table", depending + * on given param. + * + * @param array $param 'subject': table or database + * + * @return Type Description + */ + public static function create($param) { + $param = Arrayobj::make($param); + return strtolower( $param['subject'] ) === 'table' ? static::create_table($param) : static::create_database($param); + } + + public static function create_table($param) { + $param = is_array($param) ? Arrayobj::make($param) : $param; + + return static::prepare_array([ + static::$syntax['create'], + static::$syntax['table'], + $param->if_has('!exist', static::$syntax['!exist']), + static::full_tablename($param), + static::group_create_fields($param->mandatory('fields'), true), + $param->if_has('collation' , static::$syntax['collate']." {$param['collation']}" ) + ]); + } + + public static function create_database($param) { + $param = is_array($param) ? Arrayobj::make($param) : $param; + + return static::prepare_array([ + static::$syntax['create'], + static::$syntax['database'], + $param->if_has('!exist', static::$syntax['!exist']), + static::escape( $param->mandatory('database') ) + ]); + } + + public static function insert($param) { + $param = Arrayobj::make($param); + + $field_label = static::group_fields( $param->mandatory('fields'), true, true ); + $field_values = static::group_values( $param->mandatory('values'), $param['escaped'] ?: false ); + + return static::prepare_array([ + static::$syntax['insert'], + static::full_tablename($param), + $field_label, + static::$syntax['values'], + $field_values + ]); + } + + public static function grant($param) { + $param = Arrayobj::make($param); + + $field_label = static::group_fields( $param->mandatory('privileges') ); + $users = static::group_fields( $param->mandatory('users') ); + + return static::prepare_array([ + static::$syntax['grant'], + $field_label, + static::$syntax['on_table'], + static::full_tablename($param), + static::$syntax['to'], + $users, + $param->if_has('grant_option', static::$syntax['grant_option']) + ]); + } + + public static function delete($param) { + $param = Arrayobj::make($param); + + return static::prepare_array([ + static::$syntax['delete'], + static::full_tablename($param), + static::prepare_where($param['where'], false, $param->ternary('escaped', true)), + static::prepare_order($param), + static::prepare_limit($param) + ]); + } + + public static function update($param) { + $param = Arrayobj::make($param); + + $fields = static::group_values_and_fields($param->mandatory('fields'), $param->mandatory('values')); + + return static::prepare_array([ + static::$syntax['update'], + static::full_tablename($param), + static::$syntax['set'], + $fields, + static::prepare_where($param['where']) + ]); + } + + public static function drop($param) { + $param = Arrayobj::make($param); + + return static::prepare_array([ + static::$syntax['drop'], + $param->exist('table_name') ? static::$syntax['table']." ".static::full_tablename($param) : static::$syntax['database']." ".static::escape($param->mandatory('database')) + ]); + } + + public static function full_tablename($param) { + is_array($param) && ($param = Arrayobj::make($param)); + return $param->if_has('database', static::escape($param['database']).".") . static::escape($param->mandatory('table_name')) . $param->if_has('alias', " ".static::$syntax['as']." " . $param['alias']); + } + + public static function group_fields($fields, $enclose = false, $escape = false) { + if (is_array($fields)) { + return ($enclose ? "(" : "") .implode(', ', $escape ? array_map(function($item){ return static::escape($item); }, $fields) : $fields).($enclose ? ")" : ""); + } + else { + return $escape ? static::escape($fields) : $fields; + } + } + + public static function group_create_fields($fields, $enclose = false) { + if (is_array($fields)) { + $retval = []; + + foreach($fields as $key => $value) { + $retval[] = static::escape($key)." ".$value; + } + + return ($enclose ? "(" : "") .implode(', ', $retval).($enclose ? ")" : ""); + } + else { + return $fields; + } + } + + public static function group_values($values, $escaped = false) { + $tmp = array_pop($values); + array_push($values, $tmp); + + # Are we dealing with an array of values ? + if ( is_array($tmp) ) { + $retval = []; + + foreach($values as $item) { + $retval[] = implode(', ', $escaped ? $item : static::escape_values($item) ); + } + + return "(".implode('), (', $retval).")"; + } + else { + return "(".implode(', ', $escaped ? $values : static::escape_values($values)).")"; + } + } + + public static function escape_values($values) { + $type_function = function(& $item) { + + switch( $t = gettype($item) ) { + case "boolean": + $item = $item ? 1 : 0; + break; + + case "double": + case "integer": + break; + + case "NULL": + $item = "NULL"; + break; + + case "string": + $item = "\"$item\""; + break; + } + + + return $item; + }; + + return is_array($values) ? array_map($type_function, $values) : $type_function($values); + } + + public static function group_values_and_fields($fields, $values) { + $retval = []; + + foreach($fields as $key => $item) { + $retval[] = "{$item} = {$values[$key]}"; + } + + return implode(', ', $retval); + } + + public static function prepare_array($sql) { + return implode(" ", array_filter($sql)).";"; + } + + public static function prepare_where($where, $recursion = false, $escaped = false) { + $retval = []; + + if (is_array($where)) { + $count = count($where); + for($i = 0; $i < $count; $i++) { + $item = $where[$i]; + + if ( ! Arrayobj::array_is_associative($item) ) { + $retval[] = "(".static::prepare_where($item, true, $escaped).")"; + } + else { + $comparison = (isset($item['comparison']) ? $item['comparison'] : "="); + + # are we having an IN comparison here ... + if ( $is_array = (is_array($item['value']) && count($item['value']) > 1) ) { + switch ($item['comparison']) { + case '=': + $comparison = '='; + break; + + case '!=': + $comparison = 'not_in'; + break; + } + } + + $value = static::group_fields($item['value'], true); + + + switch($comparison) { + case 'match': + $retval[] = static::$syntax[$comparison].' ('.static::fieldname($item['field'], $item['alias'] ?? null).") ".static::$syntax['against']. + " (".(!$escaped || $is_array ? $value : static::escape_values($value))." IN BOOLEAN MODE)". + ($i + 1 < $count ? " ".static::$syntax[ isset($item['operator']) ? $item['operator'] : "and" ] : ""); + + break; + + default: + $retval[] = static::fieldname($item['field'], $item['alias'] ?? null)." " . ( isset(static::$syntax[$comparison]) ? static::$syntax[$comparison] : $comparison) . + " ".(!$escaped || $is_array ? $value : static::escape_values($value)). + ($i + 1 < $count ? " ".static::$syntax[ isset($item['operator']) ? $item['operator'] : "and" ] : ""); + break; + + } + } + } + } + + return $retval ? ($recursion ? "" : static::$syntax['where'] . " ") . implode(" ", $retval ) : ""; + } + + public static function prepare_join($joins) { + $retval = []; + + if ( is_array($joins) ) { + $count = count($joins); + + for($i = 0; $i < $count; $i++) { + $join = []; + + $table = Arrayobj::make([ + 'table_name' => $joins[$i]['table'], + 'alias' => $joins[$i]['alias_right'] + ]); + + $join[] = static::$syntax[ "join-".$joins[$i]['type'] ] ?? $joins[$i]['type']; + $join[] = static::$syntax[ 'join' ]; + $join[] = static::full_tablename($table); + $join[] = static::$syntax[ 'on' ]; + + foreach($joins[$i]['fields'] as $left_field => $right_field) { + #$join[] = $joins[$i]['alias_left'].".".static::escape($left_field); + $join[] = static::fieldname($left_field, $joins[$i]['alias_left']); + $join[] = $joins[$i]['comparison']; + $join[] = static::fieldname($right_field, $joins[$i]['alias_right']); + } + + $retval[] = implode(' ', $join); + } + + } + + return implode(' ', $retval); + } + + public static function prepare_order($order, $alias = null) + { + $retval = []; + + if (is_array($order)) { + foreach($order as $item) { + $retval[] = static::fieldname($item['field'], $alias).( !empty($item['order']) ? " ".$item['order'] : "" ); + } + } + + return $retval ? static::$syntax['order_by']." ".implode(', ', $retval) : ""; + } + + public static function prepare_group($group) + { + return $group ? static::$syntax['group_by']." ".( is_array($group) ? implode(', ', $group) : $group ) : ""; + } + + public static function prepare_limit($param) + { + return implode(' ', array_filter([ + $param->if_has('limit' , static::$syntax['limit'] ." {$param['limit']}"), + $param->if_has('offset', static::$syntax['offset']." {$param['offset']}") + ])); + } + + public static function fieldname($field, $alias = null) + { + return strpos($field, '.') ? $field : (!empty($alias) ? $alias."." : "").static::escape($field); + } + + public static function escape($field) + { + return static::$escape_char . str_replace(static::$escape_char, '', $field) . static::$escape_char; + } +} diff --git a/src/Modeler/Schema.php b/src/Modeler/Schema.php new file mode 100644 index 0000000..5744811 --- /dev/null +++ b/src/Modeler/Schema.php @@ -0,0 +1,21 @@ +<?php + +namespace Ulmus\Modeler; + +class Schema { + + public function __construct() + { + + } + + public function compare() + { + + } + + public function migrate() + { + + } +} diff --git a/src/Query/Explain.php b/src/Query/Explain.php new file mode 100644 index 0000000..6bd7df1 --- /dev/null +++ b/src/Query/Explain.php @@ -0,0 +1,17 @@ +<?php + +namespace Ulmus\Query; + +class Explain extends Fragment { + + public int $order = -1000; + + public bool $extended = false; + + public function render() : string + { + return $this->renderSegments([ + "EXPLAIN", $this->extended ? "EXTENDED" : "" + ]); + } +} diff --git a/src/Query/Fragment.php b/src/Query/Fragment.php new file mode 100644 index 0000000..d17c3de --- /dev/null +++ b/src/Query/Fragment.php @@ -0,0 +1,15 @@ +<?php + +namespace Ulmus\Query; + +abstract class Fragment { + + public int $order = 0; + + public abstract function render() : string; + + protected function renderSegments(array $segments, string $glue = " ") : string + { + return implode($glue, array_filter($segments)); + } +} diff --git a/src/Query/From.php b/src/Query/From.php new file mode 100644 index 0000000..5027e9c --- /dev/null +++ b/src/Query/From.php @@ -0,0 +1,43 @@ +<?php + +namespace Ulmus\Query; + +class From extends Fragment { + + public int $order = -80; + + protected $tables = []; + + public function set(array $tables) : self + { + $this->tables = $tables; + return $this; + } + + public function add($table) : self + { + foreach((array) $table as $alias => $name) { + $this->tables[$alias] = $name; + } + + return $this; + } + + public function render() : string + { + return $this->renderSegments([ + 'FROM', $this->renderTables(), + ]); + } + + protected function renderTables() : string + { + $list = []; + + foreach((array) $this->tables as $alias => $table) { + $list[] = ! is_numeric($alias) ? "`$table` $alias" : "`$table`"; + } + + return implode(", ", $list); + } +} diff --git a/src/Query/GroupBy.php b/src/Query/GroupBy.php new file mode 100644 index 0000000..0cb77e0 --- /dev/null +++ b/src/Query/GroupBy.php @@ -0,0 +1,29 @@ +<?php + +namespace Ulmus\Query; + +class GroupBy extends Fragment { + public int $order = 70; + + public array $groupBy = []; + + public function set(array $order) : self + { + $this->groupBy = $order; + return $this; + } + + public function add(string $field, ? string $direction = null) : self + { + $this->groupBy[] = $field; + return $this; + } + + public function render() : string + { + return $this->renderSegments([ + 'GROUP BY', implode(", ", $this->groupBy) + ]); + } + +} diff --git a/src/Query/Having.php b/src/Query/Having.php new file mode 100644 index 0000000..8f5ed94 --- /dev/null +++ b/src/Query/Having.php @@ -0,0 +1,7 @@ +<?php + +namespace Ulmus\Query; + +class Having extends Where { + +} diff --git a/src/Query/Insert.php b/src/Query/Insert.php new file mode 100644 index 0000000..042229c --- /dev/null +++ b/src/Query/Insert.php @@ -0,0 +1,9 @@ +<?php + +namespace Ulmus\Query; + +class Insert extends Fragment { + + public bool $ignore = false; + +} diff --git a/src/Query/Join.php b/src/Query/Join.php new file mode 100644 index 0000000..c72de34 --- /dev/null +++ b/src/Query/Join.php @@ -0,0 +1,28 @@ +<?php + +namespace Ulmus\Query; + +class Join extends Fragment { + + const TYPE_LEFT = "LEFT"; + const TYPE_RIGHT = "RIGHT"; + const TYPE_INNER = "INNER"; + const TYPE_FULL = "FULL"; + const TYPE_CROSS = "CROSS"; + const TYPE_NATURAL = "NATURAL"; + + public string $type = self::TYPE_INNER; + + public bool $outer = false; + + public function render() : string + { + return $this->renderSegments([ + $this->side, + 'JOIN', + /* table here! */, + 'ON', + /* WHERE ! */ + ]); + } +} diff --git a/src/Query/Like.php b/src/Query/Like.php new file mode 100644 index 0000000..917d780 --- /dev/null +++ b/src/Query/Like.php @@ -0,0 +1,7 @@ +<?php + +namespace Ulmus\Query; + +class Like extends Fragment { + +} diff --git a/src/Query/Limit.php b/src/Query/Limit.php new file mode 100644 index 0000000..4f8a011 --- /dev/null +++ b/src/Query/Limit.php @@ -0,0 +1,23 @@ +<?php + +namespace Ulmus\Query; + +class Limit extends Fragment { + + public int $order = 80; + + protected int $limit = 0; + + public function set($limit) : self + { + $this->limit = $limit; + return $this; + } + + public function render() : string + { + return $this->renderSegments([ + 'LIMIT', $this->limit, + ]); + } +} diff --git a/src/Query/MySQL/Replace.php b/src/Query/MySQL/Replace.php new file mode 100644 index 0000000..ee9c480 --- /dev/null +++ b/src/Query/MySQL/Replace.php @@ -0,0 +1,17 @@ +<?php + +namespace Ulmus\Query\MySQL; + +class Replace extends \Ulmus\Query\Fragment { + + public int $order = -1000; + + public bool $extended = false; + + public function render() : string + { + return $this->renderSegments([ + "EXPLAIN", $this->extended ? "EXTENDED" : "" + ]); + } +} diff --git a/src/Query/Offset.php b/src/Query/Offset.php new file mode 100644 index 0000000..5b84d2b --- /dev/null +++ b/src/Query/Offset.php @@ -0,0 +1,23 @@ +<?php + +namespace Ulmus\Query; + +class Offset extends Fragment { + + public int $order = 81; + + protected int $offset = 0; + + public function set($offset) : self + { + $this->offset = $offset; + return $this; + } + + public function render() : string + { + return $this->renderSegments([ + 'OFFSET', $this->offset, + ]); + } +} diff --git a/src/Query/OrderBy.php b/src/Query/OrderBy.php new file mode 100644 index 0000000..b88a1d4 --- /dev/null +++ b/src/Query/OrderBy.php @@ -0,0 +1,34 @@ +<?php + +namespace Ulmus\Query; + +class OrderBy extends Fragment { + public int $order = 70; + + public array $orderBy = []; + + public function set(array $order) : self + { + $this->orderBy = $order; + return $this; + } + + public function add(string $field, ? string $direction = null) : self + { + $this->orderBy[] = [ $field, $direction ]; + return $this; + } + + public function render() : string + { + $list = array_map(function($item) { + list($field, $direction) = $item; + return $field . ( $direction ? " $direction" : "" ); + }, $this->orderBy); + + return $this->renderSegments([ + 'ORDER BY', implode(", ", $list) + ]); + } + +} diff --git a/src/Query/Select.php b/src/Query/Select.php new file mode 100644 index 0000000..ecd4bd7 --- /dev/null +++ b/src/Query/Select.php @@ -0,0 +1,44 @@ +<?php + +namespace Ulmus\Query; + +class Select extends Fragment { + + public int $order = -100; + + public bool $distinct = false; + + public bool $union = false; + + public bool $top = false; + + protected $fields = []; + + public function set($fields) : self + { + $this->fields = is_array($fields) ? $fields : [ $fields ]; + return $this; + } + + public function add($fields) : self + { + if ( is_array($fields) ) { + $this->fields = array_merge($this->fields, $fields); + } + else { + $this->fields[] = $fields; + } + + return $this; + } + + public function render() : string + { + return $this->renderSegments([ + ( $this->union ? 'UNION' : false ), + 'SELECT', + ( $this->top ? 'TOP' : false ), + implode(', ', $this->fields) + ]); + } +} diff --git a/src/Query/Where.php b/src/Query/Where.php new file mode 100644 index 0000000..6c7e5b9 --- /dev/null +++ b/src/Query/Where.php @@ -0,0 +1,151 @@ +<?php + +namespace Ulmus\Query; + +use Ulmus\QueryBuilder; + +use Ulmus\Common\EntityField, + Ulmus\Common\Sql; + +class Where extends Fragment { + const OPERATOR_LIKE = "LIKE"; + const OPERATOR_EQUAL = "="; + const OPERATOR_NOT_EQUAL = "<>"; + const CONDITION_AND = "AND"; + const CONDITION_OR = "OR"; + const CONDITION_NOT = "NOT"; + const COMPARISON_IN = "IN"; + const COMPARISON_IS = "IS"; + const COMPARISON_NULL = "NULL"; + + public int $order = 50; + + public array $conditionList; + + public QueryBuilder $queryBuilder; + + public ? Where $parent = null; + + public string $condition = self::CONDITION_AND; + + public function __construct(? QueryBuilder $queryBuilder, $condition = self::CONDITION_AND) + { + $this->queryBuilder = $queryBuilder; + $this->condition = $condition; + $this->parent = $queryBuilder->where ?? null; + } + + public function add($field, $value, string $operator, string $condition, bool $not = false) : self + { + $this->conditionList[] = [ + $field, + $value, + $operator ?: $this->queryBuilder->conditionOperator, + $condition, + $not + ]; + + return $this; + } + + public function render() : string + { + $stack = []; + + foreach ($this->conditionList ?? [] as $key => $item) { + if ( $item instanceof Where ) { + if ( $item->conditionList ?? false ) { + $stack[] = ( $key !== 0 ? "{$item->condition} " : "" ) . "(" . $item->render() . ")"; + } + } + else { + list($field, $value, $operator, $condition, $not) = $item; + $stack[] = $latest = $this->whereCondition($field, $value, $operator, $key !== 0 ? $condition : "", $not); + } + } + + return $this->renderSegments([ + ! $this->parent ? "WHERE" : "", + implode(" ", $stack) + ]); + } + + protected function whereCondition($field, $value, string $operator = self::OPERATOR_EQUAL, string $condition = self::CONDITION_AND, bool $not = false) { + return new class($this->queryBuilder, $field, $value, $operator, $condition, $not) { + + public $value; + public bool $not = false; + public string $field; + public string $operator; + public string $condition; + public QueryBuilder $queryBuilder; + + protected string $content = ""; + + public function __construct(QueryBuilder $queryBuilder, string $field, $value, string $operator, string $condition, bool $not) { + $this->queryBuilder = $queryBuilder; + $this->field = $field; + $this->value = $value; + $this->condition = $condition; + $this->operator = $operator; + $this->not = $not; + } + + public function render() : string + { + $value = $this->value(); + + return $this->content ?: $this->content = implode(" ", array_filter([ + $this->condition, + $this->not ? Where::CONDITION_NOT : "", + $this->field, + $this->operator(), + $value, + ])); + } + + protected function operator() : string + { + if ( is_array($this->value) ) { + return (in_array($this->operator, [ '!=', '<>' ]) ? Where::CONDITION_NOT . " " : "") . Where::COMPARISON_IN; + } + + return $this->operator; + } + + protected function value() + { + if ( is_array($this->value) ) { + $stack = []; + + foreach($this->value as $item) { + $stack[] = $this->filterValue($item); + } + + return "(" . implode(", ", $stack) . ")"; + } + + return $this->filterValue($this->value); + } + + protected function filterValue($value) + { + if ( $value === null ) { + $this->operator = in_array($this->operator, [ '!=', '<>' ]) ? Where::COMPARISON_IS . " " . Where::CONDITION_NOT : Where::COMPARISON_IS; + return Where::COMPARISON_NULL; + } + elseif ( is_object($value) && ( $value instanceof EntityField ) ) { + return $value->name(); + } + else { + return $this->queryBuilder->addParameter($this->value); + } + } + + public function __toString() : string + { + return $this->render(); + } + }; + } +} diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php new file mode 100644 index 0000000..ac0f37a --- /dev/null +++ b/src/QueryBuilder.php @@ -0,0 +1,182 @@ +<?php + +namespace Ulmus; + + +class QueryBuilder +{ + + public Query\Where $where; + + /** + * Those are the parameters we are going to bind to PDO. + */ + public array $parameters = []; + + public string $conditionOperator = Query\Where::CONDITION_AND; + + public string $entityClass; + + protected int $parameterIndex = 0; + + protected array $queryStack = []; + + public function __construct($entityClass = "") { + $this->entityClass = $entityClass; + } + + public function select($field) : self + { + if ( $select = $this->has(Query\Select::class) ) { + $select->add($field); + } + else { + $select = new Query\Select(); + $select->set($field); + $this->push($select); + } + + return $this; + } + + public function from($table, $alias = null, $database = null) : self + { + if ( $database ) { + $table = "`$database`.".$table; + } + + if ( $from = $this->has(Query\From::class) ) { + $from->add($alias ? [ $alias => $table ] : $table); + } + else { + $from = new Query\From(); + $this->push($from); + $from->set($alias ? [ $alias => $table ] : $table); + } + + return $this; + } + + public function open(string $condition = Query\Where::CONDITION_AND) : self + { + if ( false !== ($this->where ?? false) ) { + $this->where->conditionList[] = $new = new Query\Where($this, $condition); + $this->where = $new; + } + + return $this; + } + + public function close() : self + { + if ( false !== ($this->where ?? false) && $this->where->parent ) { + $this->where = $this->where->parent; + } + + return $this; + } + + public function where($field, $value, string $operator = Query\Where::OPERATOR_EQUAL, string $condition = Query\Where::CONDITION_AND, bool $not = false) : self + { + if ( $this->where ?? false ) { + $where = $this->where; + } + elseif ( false === $where = $this->has(Query\Where::class) ) { + $this->where = $where = new Query\Where($this); + $this->push($where); + } + + $this->conditionOperator = $operator; + $where->add($field, $value, $operator, $condition, $not); + return $this; + } + + public function notWhere($field, $value, string $operator = Query\Where::CONDITION_AND) : self + { + return $this->where($field, $value, $operator, true); + } + + public function groupBy() : self + { + //$this->queryBuilder->groupBy(); + return $this; + } + + public function limit(int $value) : self + { + if ( false === $limit = $this->has(Query\Limit::class) ) { + $limit = new Query\Limit(); + $this->push($limit); + } + + $limit->set($value); + return $this; + } + + public function offset(int $value) : self + { + if ( false === $offset = $this->has(Query\Offset::class) ) { + $offset = new Query\Offset(); + $this->push($offset); + } + + $offset->set($value); + return $this; + } + + public function orderBy(string $field, ? string $direction = null) : self + { + if ( false === $orderBy = $this->has(Query\OrderBy::class) ) { + $orderBy = new Query\OrderBy(); + $this->push($orderBy); + } + + $orderBy->add($field, $direction); + return $this; + } + + public function push(Query\Fragment $queryFragment) : self + { + $this->queryStack[] = $queryFragment; + return $this; + } + + public function render() : string + { + $sql = []; + + usort($this->queryStack, function($q1, $q2) { + return $q1->order <=> $q2->order; + }); + + foreach($this->queryStack as $fragment) { + $sql[] = $fragment->render(); + } + + return implode(" ", $sql); + } + + public function has($class) { + foreach($this->queryStack as $item) { + if ( get_class($item) === $class ) { + return $item; + } + } + + return false; + } + + public function __toString() : string + { + return $this->render(); + } + + public function addParameter($value, $key = null) { + if ( $key === null ) { + $key = ":p" . $this->parameterIndex++; + } + + $this->parameters[$key] = $value; + return $key; + } +} diff --git a/src/Repository.php b/src/Repository.php new file mode 100644 index 0000000..3802ab4 --- /dev/null +++ b/src/Repository.php @@ -0,0 +1,254 @@ +<?php + +namespace Ulmus; + +use Ulmus\Common\EntityResolver; + +class Repository +{ + const DEFAULT_ALIAS = "this"; + + protected ? ConnectionAdapter $adapter; + + protected QueryBuilder $queryBuilder; + + protected EntityResolver $entityResolver; + + public string $alias; + + public string $entityClass; + + public function __construct(string $entity, string $alias = self::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) { + $this->entityClass = $entity; + $this->alias = $alias; + $this->adapter = $adapter; + $this->queryBuilder = new QueryBuilder(); + $this->entityResolver = Ulmus::resolveEntity($entity); + } + + public function loadOne() : EntityCollection + { + return $this->limit(1)->collectionFromQuery(); + } + + public function loadAll() : EntityCollection + { + return $this->collectionFromQuery(); + } + + public function loadFromPk($value) : EntityCollection + { + return $this->where('id', $value)->loadOne(); + } + + public function loadFromField($field, $value) : EntityCollection + { + return $this->where($field, $value)->collectionFromQuery(); + } + + public function yieldAll() : \Generator + { + + } + + public function select($fields) : self + { + $this->queryBuilder->select($fields); + return $this; + } + + public function from($table) : self + { + foreach((array) $table as $alias => $table) { + $this->queryBuilder->from($table, is_numeric($alias) ? null : $alias); + } + + return $this; + } + + public function join(string $type, $table, $field, $value) : self + { + return $this; + } + + public function open(string $condition = Query\Where::CONDITION_AND) : self + { + $this->queryBuilder->open($condition); + return $this; + } + + public function orOpen() : self + { + return $this->open(Query\Where::CONDITION_OR); + } + + public function close() : self + { + $this->queryBuilder->close(); + return $this; + } + + public function where($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_AND); + return $this; + } + + public function and($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + return $this->where($field, $value, $operator); + } + + public function or($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_OR); + return $this; + } + + public function notWhere(array $condition) : self + { + $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_AND, true); + return $this; + } + + public function orNot($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->notWhere($condition, Query\Where::CONDITION_OR, true); + return $this; + } + + public function having() : self + { + return $this; + } + + public function notHaving() : self + { + return $this; + } + + public function in($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->where($field, $value, $operator); + return $this; + } + + public function orIn($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + $this->queryBuilder->where($field, $value, $operator, Query\Where::CONDITION_OR); + return $this; + } + + public function notIn($field, $value) : self + { + $this->queryBuilder->where($field, $value, Query\Where::OPERATOR_NOT_EQUAL); + return $this; + } + + public function orNotIn($field, $value, string $operator = Query\Where::OPERATOR_EQUAL) : self + { + return $this->orNot($field, $value, Query\Where::OPERATOR_NOT_EQUAL, Query\Where::CONDITION_OR); + } + + public function like($field, $value) : self + { + $this->queryBuilder->where($field, $value, Query\Where::OPERATOR_LIKE, Query\Where::CONDITION_AND); + return $this; + } + + public function notLike($field, $value) : self + { + $this->queryBuilder->where($field, $value, Query\Where::OPERATOR_LIKE, Query\Where::CONDITION_AND, true); + return $this; + } + + public function match() : self + { + + } + + public function notMatch() : self + { + + } + + public function between() : self + { + + } + + public function notBetween() : self + { + + } + + public function groupBy() : self + { + #$this->queryBuilder->groupBy(); + return $this; + } + + public function orderBy(string $field, ? string $direction = null) : self + { + $this->queryBuilder->orderBy($field, $direction); + return $this; + } + + public function limit(int $value) : self + { + $this->queryBuilder->limit($value); + return $this; + } + + public function offset(int $value) : self + { + $this->queryBuilder->offset($value); + return $this; + } + + public function commit() : self + { + return $this; + } + + public function rollback() : self + { + return $this; + } + + protected function collectionFromQuery() : EntityCollection + { + $class = $this->entityClass; + + $entityCollection = new EntityCollection(); + + foreach(Ulmus::iterateQueryBuilder($this->selectSqlQuery()->queryBuilder) as $entityData) { + $entityCollection->append( ( new $class() )->entityFillFromDataset($entityData) ); + } + + return $entityCollection; + } + + protected function selectSqlQuery() : self + { + if ( ! $this->queryBuilder->has(Query\Select::class) ) { + $this->select("{$this->alias}.*"); + } + + if ( ! $this->queryBuilder->has(Query\From::class) ) { + $this->from([ $this->alias => $this->entityResolver->tableName() ]); + } + + return $this; + } + + protected function fromRow($row) : self + { + + } + + protected function fromCollection($rows) : self + { + + } +} diff --git a/src/Ulmus.php b/src/Ulmus.php new file mode 100644 index 0000000..f7f322e --- /dev/null +++ b/src/Ulmus.php @@ -0,0 +1,53 @@ +<?php + +namespace Ulmus; + +use Generator; + +abstract class Ulmus +{ + public static string $repositoryClass = "\\Ulmus\\Repository"; + + public static string $queryBuilderClass = "\\Ulmus\\QueryBuilder"; + + public static ConnectionAdapter $defaultAdapter; + + public static array $resolved = []; + + protected static function fetchQueryBuilder(QueryBuilder $queryBuilder, ?ConnectionAdapter $adapter = null) : array + { + $sql = $queryBuilder->render(); + return ( $adapter ?: static::$defaultAdapter )->pdo->select($sql, $queryBuilder->parameters ?? [])->fetchAll(); + } + + public static function iterateQueryBuilder(QueryBuilder $queryBuilder, ?ConnectionAdapter $adapter = null) : Generator + { + $sql = $queryBuilder->render(); + $statement = ( $adapter ?: static::$defaultAdapter )->pdo->select($sql, $queryBuilder->parameters ?? []); + + while ( $row = $statement->fetch() ) { + yield $row; + } + + $statement->closeCursor(); + + return [ + 'count' => $statement->rowCount(), + ]; + } + + public static function resolveEntity(string $entityClass) : Common\EntityResolver + { + return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass); + } + + public static function repository(...$arguments) : Repository + { + return new static::$repositoryClass(...$arguments); + } + + public static function queryBuilder(...$arguments) : QueryBuilder + { + return new static::$queryBuilderClass(...$arguments); + } +}