- First draft of current WIP
This commit is contained in:
commit
b73d046e0a
|
@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Adapter;
|
||||
|
||||
use Ulmus\Common\PdoObject;
|
||||
|
||||
interface AdapterInterface {
|
||||
public function connect() : PdoObject;
|
||||
public function buildDataSourceName() : string;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Adapter;
|
||||
|
||||
class MariaDB extends MySQL {
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Annotation;
|
||||
|
||||
interface Annotation {}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Annotation\Classes;
|
||||
|
||||
class Collation implements \Ulmus\Annotation\Annotation {
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Annotation\Classes;
|
||||
|
||||
class Function implements \Ulmus\Annotation\Annotation {
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus;
|
||||
|
||||
class EntityCollection extends \ArrayObject
|
||||
{
|
||||
use Common\ArrayObjectTrait;
|
||||
}
|
|
@ -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,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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Modeler;
|
||||
|
||||
class Schema {
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function compare()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function migrate()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -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" : ""
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Query;
|
||||
|
||||
class Having extends Where {
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Query;
|
||||
|
||||
class Insert extends Fragment {
|
||||
|
||||
public bool $ignore = false;
|
||||
|
||||
}
|
|
@ -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 ! */
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?php
|
||||
|
||||
namespace Ulmus\Query;
|
||||
|
||||
class Like extends Fragment {
|
||||
|
||||
}
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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" : ""
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue