diff --git a/docs/00-intro.md b/docs/00-intro.md new file mode 100644 index 0000000..bacf7ff --- /dev/null +++ b/docs/00-intro.md @@ -0,0 +1,95 @@ +# Ulmus + +Welcome to the official Ulmus documentation. + +## Quick start + +Creating a simple user entity: + +*/app/entity/user.php* +```php + "user") + */ +class User +{ + use \Ulmus\EntityTrait; + + /** + * @Id + */ + public int $id; + + /** + * @Field + */ + public string $fullname; + + /** + * @Field + */ + public ? string $email; + + /** + * @Field('name' => 'is_admin') + */ + public bool $isAdmin = false; + + /** + * @DateTime + */ + public Datetime $birthday; +} +``` + +### Loading a user using a Primary Key + +With this entity, we could quickly load a user using the `@Id` field using: + +```php +$user = Entity\User::repository()->loadFromPk((int) $_GET['id']); +``` + +Or we could also, load another single entity using a specific field: + +```php +# fetch a single user +$user = Entity\User::repository()->loadOneFromField((string) $_POST['email'], 'email'); + +# Fetch every admins +$admin_list = Entity\User::repository()->loadFromField(true, 'isAdmin'); +``` + +Using the same entity class, we could create a new user and save it using: + +```php +$user = new Entity\User(); +$user->fullname = "Johnny Does"; +$user->birthday = "1980-01-15"; + +if ( Entity\User::repository()->save($user) ) { + echo "User created successfully !"; +} +else { + echo "User could not be saved :\"; +} +``` + +Which would result in the following query (from MySQL adapter) being generated and executed : + +```SQL +INSERT INTO `user` VALUES fullname = :fullname, birthday = :birthday; +``` + +Binded using: + +```PHP +'fullname' => "Johnny Does", +'birthday' => "1980-01-15", +``` diff --git a/docs/01-load.md b/docs/01-load.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/01-save.md b/docs/01-save.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/10-join.md b/docs/10-join.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/10-relation.md b/docs/10-relation.md new file mode 100644 index 0000000..e69de29 diff --git a/src/Adapter/AdapterInterface.php b/src/Adapter/AdapterInterface.php index b1356fe..fd4b655 100644 --- a/src/Adapter/AdapterInterface.php +++ b/src/Adapter/AdapterInterface.php @@ -19,7 +19,8 @@ interface AdapterInterface { public function defaultEngine() : ? string; public function writableValue(/* mixed */ $value); /*: mixed*/ - + + /* public function databaseName() : string; public function mapFieldType(FieldDefinition $field) : string; public function schemaTable(string $databaseName, string $tableName) /*: object|EntityCollection diff --git a/src/Adapter/DefaultAdapterTrait.php b/src/Adapter/DefaultAdapterTrait.php index 00f6fa0..a8fe13a 100644 --- a/src/Adapter/DefaultAdapterTrait.php +++ b/src/Adapter/DefaultAdapterTrait.php @@ -2,7 +2,7 @@ namespace Ulmus\Adapter; -use Ulmus\{Entity\InformationSchema\Table, Migration\FieldDefinition, Repository, QueryBuilder}; +use Ulmus\{ConnectionAdapter, Entity\InformationSchema\Table, Migration\FieldDefinition, Repository, QueryBuilder}; trait DefaultAdapterTrait { @@ -30,9 +30,9 @@ trait DefaultAdapterTrait return $this->database; } - public function schemaTable(string $databaseName, string $tableName) /* : ? object */ + public function schemaTable(ConnectionAdapter $parent, $databaseName, string $tableName) /* : ? object */ { - return Table::repository()->where(Table::field('schema'), $databaseName)->loadOneFromField(Table::field('name'), $tableName); + return Table::repository(Repository::DEFAULT_ALIAS, $parent)->where($this->escapeIdentifier('table_schema', AdapterInterface::IDENTIFIER_FIELD), $databaseName)->loadOneFromField($this->escapeIdentifier('table_name', AdapterInterface::IDENTIFIER_FIELD), $tableName); } public function mapFieldType(FieldDefinition $field, bool $typeOnly = false) : string diff --git a/src/Adapter/MsSQL.php b/src/Adapter/MsSQL.php index fa4df77..1d88805 100644 --- a/src/Adapter/MsSQL.php +++ b/src/Adapter/MsSQL.php @@ -83,6 +83,9 @@ class MsSQL implements AdapterInterface { catch(PDOException $ex){ throw $ex; } + finally { + $this->password = str_repeat('*', random_int(8,16)); + } return $pdo; } @@ -212,9 +215,12 @@ class MsSQL implements AdapterInterface { } - public function writableValue(/* mixed */ $value) /*: mixed*/ + public function writableValue(mixed $value) /*: mixed*/ { switch (true) { + case $value instanceof \UnitEnum: + return Ulmus::convertEnum($value); + case is_object($value): return Ulmus::convertObject($value); diff --git a/src/Adapter/MySQL.php b/src/Adapter/MySQL.php index d4899e1..ab31bf9 100644 --- a/src/Adapter/MySQL.php +++ b/src/Adapter/MySQL.php @@ -77,7 +77,7 @@ class MySQL implements AdapterInterface { throw $ex; } finally { - $this->password = str_repeat('*', strlen($this->password)); + $this->password = str_repeat('*', random_int(8,16)); } return $pdo; @@ -148,9 +148,12 @@ class MySQL implements AdapterInterface { } } - public function writableValue(/* mixed */ $value) /*: mixed*/ + public function writableValue(mixed $value) : mixed { switch (true) { + case $value instanceof \UnitEnum: + return Ulmus::convertEnum($value); + case is_object($value): return Ulmus::convertObject($value); diff --git a/src/Adapter/SQLite.php b/src/Adapter/SQLite.php index 8edb808..55d8692 100644 --- a/src/Adapter/SQLite.php +++ b/src/Adapter/SQLite.php @@ -87,7 +87,7 @@ class SQLite implements AdapterInterface { return substr($base, 0, strrpos($base, '.') ?: strlen($base)); } - public function schemaTable(string $databaseName, string $tableName) /* : ? object */ + public function schemaTable(string $databaseName, string $tableName) : null|object { return Table::repository()->loadOneFromField(Table::field('tableName'), $tableName); } @@ -131,9 +131,12 @@ class SQLite implements AdapterInterface { return $typeOnly ? $type : $type . ( $length ? "($length" . ( isset($precision) ? ",$precision" : "" ) . ")" : "" ); } - public function writableValue(/* mixed */ $value) /*: mixed*/ + public function writableValue(mixed $value) /*: mixed*/ { switch (true) { + case $value instanceof \UnitEnum: + return Ulmus::convertEnum($value); + case is_object($value): return Ulmus::convertObject($value); @@ -168,6 +171,8 @@ class SQLite implements AdapterInterface { public function exportFunctions(PdoObject $pdo) : void { + $pdo->sqliteCreateFunction('if', fn($comparison, $yes, $no) => $comparison ? $yes : $no, 3); + $pdo->sqliteCreateFunction('length', fn($string) => strlen($string), 1); $pdo->sqliteCreateFunction('lcase', fn($string) => strtolower($string), 1); $pdo->sqliteCreateFunction('ucase', fn($string) => strtoupper($string), 1); $pdo->sqliteCreateFunction('left', fn($string, $length) => substr($string, 0, $length), 2); diff --git a/src/ConnectionAdapter.php b/src/ConnectionAdapter.php index db6ea72..899305b 100644 --- a/src/ConnectionAdapter.php +++ b/src/ConnectionAdapter.php @@ -38,7 +38,7 @@ class ConnectionAdapter $this->adapter->setup($connection); - unset($this->configuration['connections'][$this->name]); + unset($this->configuration['connections']); } public function getConfiguration() : array @@ -71,7 +71,7 @@ class ConnectionAdapter { return $this->pdo(); } - + /** * Instanciate an adapter which interact with the data source * @param string $name An Ulmus adapter or full class name implementing AdapterInterface diff --git a/src/Entity/ObjectInstanciator.php b/src/Entity/ObjectInstanciator.php index 14dc105..b889cce 100644 --- a/src/Entity/ObjectInstanciator.php +++ b/src/Entity/ObjectInstanciator.php @@ -27,7 +27,16 @@ class ObjectInstanciator { return (string) $obj; } - + + public function enum(\UnitEnum $obj) + { + if (! $obj instanceof \BackedEnum) { + throw new \Ulmus\Exception\BackedEnumRequired("Unable to extract a UnitEnum value from this variable. You must define your enum as a BackedEnum instead of an UnitEnum."); + } + + return $obj->value; + } + public function registerObject(string $type, Callable $callback) : void { $this->objectCallbackDefinition[$type] = $callback; diff --git a/src/Entity/Sqlite/Column.php b/src/Entity/Sqlite/Column.php new file mode 100644 index 0000000..c36d0f1 --- /dev/null +++ b/src/Entity/Sqlite/Column.php @@ -0,0 +1,48 @@ + "notnull") + */ + public bool $notNull; + + /** + * @Field('name' => 'dflt_value') + */ + public ? string $defaultValue; + + /** + * @Field('name' => "pk") + */ + public bool $primaryKey; +} \ No newline at end of file diff --git a/src/Entity/Sqlite/Table.php b/src/Entity/Sqlite/Table.php index 43652fd..9ae8f4c 100644 --- a/src/Entity/Sqlite/Table.php +++ b/src/Entity/Sqlite/Table.php @@ -2,13 +2,14 @@ namespace Ulmus\Entity\Sqlite; +use Ulmus\ConnectionAdapter; use Ulmus\Repository; class Table extends Schema { - public static function repository(string $alias = Repository::DEFAULT_ALIAS): Repository + public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null): Repository { - return new class(static::class, $alias) extends Repository\SqliteRepository + return new class(static::class, $alias, $adapter) extends Repository\SqliteRepository { public function finalizeQuery(): void { diff --git a/src/EntityTrait.php b/src/EntityTrait.php index 160c0a6..36719f2 100644 --- a/src/EntityTrait.php +++ b/src/EntityTrait.php @@ -78,11 +78,19 @@ trait EntityTrait { $this->{$field['name']} = $value; } + elseif ( $value instanceof \UnitEnum ) { + $this->{$field['name']} = $value; + } + elseif (enum_exists($field['type'])) { + $this->{$field['name']} = $field['type']::from($value); + } elseif ( ! $field['builtin'] ) { try { $this->{$field['name']} = Ulmus::instanciateObject($field['type'], [ $value ]); } catch(\Error $e) { + $f = $field['type']; + dump($f, $f::from($value)); throw new \Error(sprintf("%s for class '%s' on field '%s'", $e->getMessage(), get_class($this), $field['name'])); } } @@ -99,7 +107,7 @@ trait EntityTrait { $this->entityLoadedDataset = array_change_key_case($dataset, \CASE_LOWER) + $this->entityLoadedDataset; } } - + return $this; } @@ -285,9 +293,9 @@ trait EntityTrait { /** * @Ignore */ - public static function repository(string $alias = Repository::DEFAULT_ALIAS) : Repository + public static function repository(string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) : Repository { - return Ulmus::repository(static::class, $alias); + return Ulmus::repository(static::class, $alias, $adapter); } /** @@ -301,7 +309,6 @@ trait EntityTrait { return $collection; } - /** * @Ignore */ diff --git a/src/Exception/BackedEnumRequired.php b/src/Exception/BackedEnumRequired.php new file mode 100644 index 0000000..cf3ae95 --- /dev/null +++ b/src/Exception/BackedEnumRequired.php @@ -0,0 +1,5 @@ +show = $show; + + if ( $from !== null ) { + $this->from = $from; + } + + if ( $in !== null ) { + $this->in = $in; + } + + return $this; + } + + public function render() : string + { + return $this->renderSegments([ + static::SQL_TOKEN, $this->show, + ] + ( ! empty($this->from) ? [ static::SQL_TOKEN_FROM, $this->from ] : [] ) + ( ! empty($this->in) ? [ static::SQL_TOKEN_IN, $this->in ] : [] ) ); + } + +} diff --git a/src/QueryBuilder.php b/src/QueryBuilder.php index dc70f27..ed7004e 100644 --- a/src/QueryBuilder.php +++ b/src/QueryBuilder.php @@ -314,6 +314,39 @@ class QueryBuilder implements Query\QueryBuilderInterface return $join; } + public function showDatabases() : self + { + $show = new Query\Show(); + + $this->push($show); + + $show->set(Query\Show::SQL_SHOW_DATABASES); + + return $this; + } + + public function showTables(? string $database = null) : self + { + $show = new Query\Show(); + + $this->push($show); + + $show->set(Query\Show::SQL_SHOW_TABLES, $database); + + return $this; + } + + public function showColumns(string $table) : self + { + $show = new Query\Show(); + + $this->push($show); + + $show->set(Query\Show::SQL_SHOW_COLUMNS, $table); + + return $this; + } + public function truncate(string $table, ? string $alias = null, ? string $database = null, ? string $schema = null) : self { if ( $schema ) { diff --git a/src/QueryBuilder/SqliteQueryBuilder.php b/src/QueryBuilder/SqliteQueryBuilder.php index 821df22..6c88e5a 100644 --- a/src/QueryBuilder/SqliteQueryBuilder.php +++ b/src/QueryBuilder/SqliteQueryBuilder.php @@ -21,4 +21,22 @@ class SqliteQueryBuilder extends QueryBuilder implements Query\QueryBuilderInter return $this; } + + public function showColumns(string $table) : self + { + $this->pragma('table_info', $table, true); + + return $this; + } + + public function showTables(? string $database = null) : self + { + $show = new Query\Show(); + + $this->push($show); + + $show->set(Query\Show::SQL_SHOW_TABLES, $database); + + return $this; + } } diff --git a/src/Repository.php b/src/Repository.php index c581dd2..4a03ddb 100644 --- a/src/Repository.php +++ b/src/Repository.php @@ -38,7 +38,7 @@ class Repository public function loadOne() : ? object { - return $this->limit(1)->collectionFromQuery()[0] ?? null; + return $this->limit(1)->selectSqlQuery()->collectionFromQuery()[0] ?? null; } public function loadOneFromField($field, $value) : ? object @@ -53,7 +53,7 @@ class Repository public function loadAll() : EntityCollection { - return $this->collectionFromQuery(); + return $this->selectSqlQuery()->collectionFromQuery(); } public function loadAllFromField($field, $value, $operator= Query\Where::OPERATOR_EQUAL) : EntityCollection @@ -63,7 +63,7 @@ class Repository public function loadFromField($field, $value, $operator= Query\Where::OPERATOR_EQUAL) : EntityCollection { - return $this->where($field, $value, $operator)->collectionFromQuery(); + return $this->selectSqlQuery()->where($field, $value, $operator)->collectionFromQuery(); } public function count() : int @@ -278,7 +278,7 @@ class Repository $this->finalizeQuery(); - $result = Ulmus::runSelectQuery($this->queryBuilder, $this->adapter); + # ??? $result = Ulmus::runSelectQuery($this->queryBuilder, $this->adapter); return $this; } @@ -288,12 +288,26 @@ class Repository return $this->createSqlQuery()->runQuery(); } + public function listTables(? string $database = null) + { + return $this->showTablesSqlQuery($database)->runQuery(); + } + + public function listColumns(? string $table = null) + { + $table ??= $this->entityResolver->tableName(); + + $this->showColumnsSqlQuery($table); + + return $this->collectionFromQuery(Entity\InformationSchema\Column::class); + } + public function generateDatasetDiff(object $entity, bool $oldValues = false) : array { $array = array_change_key_case($entity->toArray()); $dataset = array_change_key_case($entity->entityGetDataset(false, true)); - +# dump($array, $dataset); return array_udiff_assoc($oldValues ? $dataset : $array , $oldValues ? $array : $dataset, function($e1, $e2) { if ( is_array($e1) ) { if (is_array($e2)) { @@ -584,7 +598,9 @@ class Repository if ( null === $entity::resolveEntity()->searchFieldAnnotation($field['name'], new RelationIgnore) ) { $escAlias = $this->escapeIdentifier($alias); - $this->select("$escAlias.$key as $alias\${$field['name']}"); + $name = $entity::resolveEntity()->searchFieldAnnotation($field['name'], new Field())->name ?? $field['name']; + + $this->select("$escAlias.$key as $alias\${$name}"); } } @@ -785,14 +801,12 @@ class Repository public function collectionFromQuery(? string $entityClass = null) : EntityCollection { - $class = $entityClass ?: $this->entityClass; + $class = $entityClass ?? $this->entityClass; - $entityCollection = $this->instanciateEntityCollection(); - - $this->selectSqlQuery(); + $entityCollection = $class::entityCollection(); $this->finalizeQuery(); - + foreach(Ulmus::iterateQueryBuilder($this->queryBuilder, $this->adapter) as $entityData) { $entityCollection->append( ( new $class() )->resetVirtualProperties()->entityFillFromDataset($entityData) ); } @@ -916,6 +930,33 @@ class Repository return $this; } + public function showDatabasesSqlQuery() : self + { + if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { + $this->queryBuilder->showDatabases(); + } + + return $this; + } + + public function showTablesSqlQuery() : self + { + if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { + $this->queryBuilder->showTables(); + } + + return $this; + } + + public function showColumnsSqlQuery(string $table) : self + { + if ( null === $this->queryBuilder->getFragment(Query\Show::class) ) { + $this->queryBuilder->showColumns($table); + } + + return $this; + } + protected function fromRow($row) : self { diff --git a/src/Repository/SqliteRepository.php b/src/Repository/SqliteRepository.php index 6392fe1..c8800d7 100644 --- a/src/Repository/SqliteRepository.php +++ b/src/Repository/SqliteRepository.php @@ -2,7 +2,7 @@ namespace Ulmus\Repository; -use Ulmus\{ConnectionAdapter, QueryBuilder, Repository, Query, Ulmus}; +use Ulmus\{ConnectionAdapter, QueryBuilder, Repository, Query, Ulmus, Entity}; class SqliteRepository extends Repository { @@ -18,6 +18,15 @@ class SqliteRepository extends Repository { } + public function listColumns(? string $table = null) + { + $table ??= $this->entityResolver->tableName(); + $this->showColumnsSqlQuery($table); + + return $this->collectionFromQuery(Entity\Sqlite\Column::class)->iterate(fn($e) => $e->tableName = $table); + } + + protected function serverRequestCountRepository() : Repository { return new static($this->entityClass, $this->alias, $this->adapter); diff --git a/src/Ulmus.php b/src/Ulmus.php index 89a9696..bb2c914 100644 --- a/src/Ulmus.php +++ b/src/Ulmus.php @@ -107,11 +107,12 @@ abstract class Ulmus return static::$resolved[$entityClass] ?? static::$resolved[$entityClass] = new Common\EntityResolver($entityClass); } - public static function repository(string $entityClass, ...$arguments) : Repository + public static function repository(string $entityClass, string $alias = Repository::DEFAULT_ALIAS, ConnectionAdapter $adapter = null) : Repository { - $cls = $entityClass::resolveEntity()->sqlAdapter()->adapter()->repositoryClass(); + $adapter ??= $entityClass::resolveEntity()->sqlAdapter(); + $cls = $adapter->adapter()->repositoryClass(); - return new $cls($entityClass, ...$arguments); + return new $cls($entityClass, $alias, $adapter); } public static function queryBuilder($entityClass, ...$arguments) : Query\QueryBuilderInterface @@ -130,12 +131,17 @@ abstract class Ulmus { return ( static::$objectInstanciator ?? static::$objectInstanciator = new Entity\ObjectInstanciator() )->convert($obj); } - + + public static function convertEnum(\UnitEnum $enum) + { + return ( static::$objectInstanciator ?? static::$objectInstanciator = new Entity\ObjectInstanciator() )->enum($enum); + } + public static function encodeArray(array $array) { return json_encode($array); } - + public static function registerAdapter(ConnectionAdapter $adapter, bool $default = false) : void { if ($default) {