- First draft of current WIP

This commit is contained in:
Dave M. 2019-08-21 16:13:00 -04:00
commit b73d046e0a
47 changed files with 2797 additions and 0 deletions

18
composer.json Normal file
View File

@ -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/"
}
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Ulmus\Adapter;
use Ulmus\Common\PdoObject;
interface AdapterInterface {
public function connect() : PdoObject;
public function buildDataSourceName() : string;
}

7
src/Adapter/MariaDB.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace Ulmus\Adapter;
class MariaDB extends MySQL {
}

77
src/Adapter/MySQL.php Normal file
View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
<?php
namespace Ulmus\Annotation;
interface Annotation {}

View File

@ -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();
}
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace Ulmus\Annotation\Classes;
class Collation implements \Ulmus\Annotation\Annotation {
}

View File

@ -0,0 +1,7 @@
<?php
namespace Ulmus\Annotation\Classes;
class Function implements \Ulmus\Annotation\Annotation {
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}
}

View File

@ -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;
}
}

45
src/Common/PdoObject.php Normal file
View File

@ -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;
}
}

67
src/Common/Sql.php Normal file
View File

@ -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
{
}
}

87
src/ConnectionAdapter.php Normal file
View File

@ -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();
}
}

8
src/EntityCollection.php Normal file
View File

@ -0,0 +1,8 @@
<?php
namespace Ulmus;
class EntityCollection extends \ArrayObject
{
use Common\ArrayObjectTrait;
}

82
src/EntityTrait.php Normal file
View File

@ -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";
}
}

0
src/Modeler/Field.php Normal file
View File

404
src/Modeler/Query.php Normal file
View File

@ -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;
}
}

21
src/Modeler/Schema.php Normal file
View File

@ -0,0 +1,21 @@
<?php
namespace Ulmus\Modeler;
class Schema {
public function __construct()
{
}
public function compare()
{
}
public function migrate()
{
}
}

17
src/Query/Explain.php Normal file
View File

@ -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" : ""
]);
}
}

15
src/Query/Fragment.php Normal file
View File

@ -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));
}
}

43
src/Query/From.php Normal file
View File

@ -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);
}
}

29
src/Query/GroupBy.php Normal file
View File

@ -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)
]);
}
}

7
src/Query/Having.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace Ulmus\Query;
class Having extends Where {
}

9
src/Query/Insert.php Normal file
View File

@ -0,0 +1,9 @@
<?php
namespace Ulmus\Query;
class Insert extends Fragment {
public bool $ignore = false;
}

28
src/Query/Join.php Normal file
View File

@ -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 ! */
]);
}
}

7
src/Query/Like.php Normal file
View File

@ -0,0 +1,7 @@
<?php
namespace Ulmus\Query;
class Like extends Fragment {
}

23
src/Query/Limit.php Normal file
View File

@ -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,
]);
}
}

View File

@ -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" : ""
]);
}
}

23
src/Query/Offset.php Normal file
View File

@ -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,
]);
}
}

34
src/Query/OrderBy.php Normal file
View File

@ -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)
]);
}
}

44
src/Query/Select.php Normal file
View File

@ -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)
]);
}
}

151
src/Query/Where.php Normal file
View File

@ -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();
}
};
}
}

182
src/QueryBuilder.php Normal file
View File

@ -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;
}
}

254
src/Repository.php Normal file
View File

@ -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
{
}
}

53
src/Ulmus.php Normal file
View File

@ -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);
}
}