commit b73d046e0ab04e92fbccc839ffa150863f9cb104 Author: Dave Mc Nicoll Date: Wed Aug 21 16:13:00 2019 -0400 - First draft of current WIP 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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ + $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 @@ + '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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +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 @@ +"; + 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 @@ +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 @@ +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 @@ +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); + } +}