From 2205dfc8e970450e524319b51d63c87904d4581b Mon Sep 17 00:00:00 2001 From: mychidarko Date: Wed, 11 Dec 2024 22:06:22 +0000 Subject: [PATCH 01/29] feat: add config for data --- composer.json | 3 ++- src/Core.php | 34 ++++++++++++++++++---------------- src/globals/config.php | 1 + 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 7c895f2..d7ad5d1 100644 --- a/composer.json +++ b/composer.json @@ -38,6 +38,7 @@ "doctrine/dbal": "^3.2", "vlucas/phpdotenv": "^5.4", "illuminate/database": "^8.75", - "illuminate/events": "^8.75" + "illuminate/events": "^8.75", + "symfony/yaml": "^6.4" } } diff --git a/src/Core.php b/src/Core.php index 95c2e59..d2d2c2c 100644 --- a/src/Core.php +++ b/src/Core.php @@ -65,25 +65,27 @@ public static function loadApplicationConfig() \Leaf\Vite::config('hotFile', 'public/hot'); } - Config::attachView(ViewConfig('viewEngine'), 'template'); - - if (ViewConfig('config')) { - call_user_func_array(ViewConfig('config'), [ - app()->template(), - [ + if (ViewConfig('viewEngine')) { + Config::attachView(ViewConfig('viewEngine'), 'template'); + + if (ViewConfig('config')) { + call_user_func_array(ViewConfig('config'), [ + app()->template(), + [ + 'views' => AppConfig('views.path'), + 'cache' => AppConfig('views.cachePath'), + ] + ]); + } else if (method_exists(app()->template(), 'configure')) { + app()->template()->configure([ 'views' => AppConfig('views.path'), 'cache' => AppConfig('views.cachePath'), - ] - ]); - } else if (method_exists(app()->template(), 'configure')) { - app()->template()->configure([ - 'views' => AppConfig('views.path'), - 'cache' => AppConfig('views.cachePath'), - ]); - } + ]); + } - if (is_callable(ViewConfig('extend'))) { - call_user_func_array(ViewConfig('extend'), app()->template()); + if (is_callable(ViewConfig('extend'))) { + call_user_func_array(ViewConfig('extend'), app()->template()); + } } } } diff --git a/src/globals/config.php b/src/globals/config.php index b7cbb86..dcd443e 100644 --- a/src/globals/config.php +++ b/src/globals/config.php @@ -19,6 +19,7 @@ function PathsConfig($setting = null) 'channels' => 'app/channels', 'components' => 'app/components', 'controllers' => 'app/controllers', + 'database' => 'app/database', 'databaseStorage' => 'storage/app/db', 'exceptions' => 'app/exceptions', 'events' => 'app/events', From 671b1a0841934e46f87c6f521b0e886ae2e5a7b7 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Wed, 11 Dec 2024 22:10:45 +0000 Subject: [PATCH 02/29] feat: add wip schema parser --- src/Schema.php | 419 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 281 insertions(+), 138 deletions(-) diff --git a/src/Schema.php b/src/Schema.php index b7b0ca2..d13976f 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -2,186 +2,329 @@ namespace Leaf; +use Illuminate\Database\Capsule\Manager; use Illuminate\Database\Schema\Blueprint; - +use Symfony\Component\Yaml\Yaml; + +/** + * Leaf DB Schema [WIP] + * --- + * One file to rule them all. + * + * @version 1.0 + */ class Schema { - public static $capsule; + /**@var \Illuminate\Database\Capsule\Manager $capsule */ + protected static Manager $connection; /** - * @param string $table The name of table to manipulate - * @param string|null $schema The JSON schema for database + * Migrate your schema file tables + * + * @param string $fileToMigrate The schema file to migrate + * @return bool */ - public static function build(string $table, ?string $schema = null) + public static function migrate(string $fileToMigrate): bool { - list($table, $schema) = static::getSchema($table, $schema); + $data = Yaml::parseFile($fileToMigrate); + $tableName = rtrim(path($fileToMigrate)->basename(), '.yml'); - if (is_array($schema)) { - $schema = $schema[0]; + if ($data['truncate'] ?? false) { + static::$connection::schema()->dropIfExists($tableName); } - if (!static::$capsule::schema()->hasTable($table)) { - static::$capsule::schema()->create($table, function (Blueprint $table) use ($schema) { - foreach ($schema as $key => $value) { - list($key, $type) = static::getColumns($key, $value); + try { + if (!static::$connection::schema()->hasTable($tableName)) { + if (storage()->exists(StoragePath("database/$tableName"))) { + storage()->delete(StoragePath("database/$tableName")); + } - echo $key . " => " . $type . "\n"; + static::$connection::schema()->create($tableName, function (Blueprint $table) use ($data) { + $columns = $data['columns'] ?? []; + $relationships = $data['relationships'] ?? []; - if (strpos($key, '*') === 0) { - $table->foreignId(substr($key, 1)); - continue; + $increments = $data['increments'] ?? true; + $timestamps = $data['timestamps'] ?? true; + $softDeletes = $data['softDeletes'] ?? false; + $rememberToken = $data['remember_token'] ?? false; + + if ($increments) { + $table->increments('id'); } - if ($key === 'timestamps') { - $table->timestamps(); - continue; + foreach ($relationships as $model) { + $table->foreignIdFor($model); } - if ($key === 'softDeletes') { - $table->softDeletes(); - continue; + foreach ($columns as $columnName => $columnValue) { + if (is_string($columnValue)) { + $table->{$columnValue}($columnName); + } + + if (is_array($columnValue)) { + $column = $table->{$columnValue['type']}($columnName); + + unset($columnValue['type']); + + foreach ($columnValue as $columnOptionName => $columnOptionValue) { + if (is_bool($columnOptionValue)) { + $column->{$columnOptionName}(); + } else { + $column->{$columnOptionName}($columnOptionValue); + } + } + } } - if ($key === 'rememberToken') { + if ($rememberToken) { $table->rememberToken(); - continue; } - if ($type === 'enum') { - if (substr($key, -1) === '?') { - $table->enum(substr($key, 0, -1), $value)->nullable(); - continue; - } - - $table->enum($key, $value); - continue; + if ($softDeletes) { + $table->softDeletes(); } - if (method_exists($table, $type)) { - if (substr($key, -1) === '?') { - call_user_func_array([$table, $type], [substr($key, 0, -1)])->nullable(); - continue; + if ($timestamps) { + $table->timestamps(); + } + }); + } else if (storage()->exists(StoragePath("database/$tableName"))) { + static::$connection::schema()->table($tableName, function (Blueprint $table) use ($data, $tableName) { + $columns = $data['columns'] ?? []; + $relationships = $data['relationships'] ?? []; + + $allPreviousMigrations = glob(StoragePath("database/$tableName/*.yml")); + $lastMigration = $allPreviousMigrations[count($allPreviousMigrations) - 1] ?? null; + $lastMigration = Yaml::parseFile($lastMigration); + + $increments = $data['increments'] ?? true; + $timestamps = $data['timestamps'] ?? true; + $softDeletes = $data['softDeletes'] ?? false; + $rememberToken = $data['remember_token'] ?? false; + + if ($increments !== ($lastMigration['increments'] ?? true)) { + if ($increments && !static::$connection::schema()->hasColumn($tableName, 'id')) { + $table->increments('id'); + } else if (!$increments && static::$connection::schema()->hasColumn($tableName, 'id')) { + $table->dropColumn('id'); } + } - call_user_func_array([$table, $type], [$key]); - continue; + $columnsDiff = []; + $staticColumns = []; + $removedColumns = []; + + foreach ($lastMigration['columns'] as $colKey => $colVal) { + if (!array_key_exists($colKey, $columns)) { + $removedColumns[] = $colKey; + } else if (static::getColumnAttributes($colVal) !== static::getColumnAttributes($columns[$colKey])) { + $columnsDiff[] = $colKey; + $staticColumns[] = $colKey; + } else { + $staticColumns[] = $colKey; + } } - } - }); - } - } - /** - * Get the table and table structure - * @param string $table The name of table to manipulate (can be the name of the file) - * @param string|null $schema The JSON schema for database (if $table is not the name of the file) - * - * @return array - */ - protected static function getSchema(string $table, ?string $schema = null): array - { - try { - if ($schema === null) { - if (file_exists($table)) { - $schema = file_get_contents($table); - $table = str_replace('.json', '', basename($table)); - } else { - $table = str_replace('.json', '', $table); - $schema = json_decode(file_get_contents(AppPaths('schema') . "/$table.json")); - } - } else { - $schema = json_decode($schema); - } + $newColumns = array_diff(array_keys($columns), $staticColumns); - return [$table, $schema]; - } catch (\Throwable $th) { - throw $th; - } - } + if (count($newColumns) > 0) { + foreach ($newColumns as $newColumn) { + $column = static::getColumnAttributes($columns[$newColumn]); - /** - * Get the columns of a table and their types - * - * @param string $key The column as provided in the schema - * @param mixed $value The value of the column as provided in the schema - * - * @return array - */ - protected static function getColumns(string $key, $value): array - { - $type = ''; - $column = ''; - - $keyData = explode(':', $key); - - if (count($keyData) > 1) { - $type = trim($keyData[1]); - $column = trim($keyData[0]); - - if ($type === 'id') { - $column .= '*'; - $type = 'bigIncrements'; - } else if ($type === 'number') { - $type = 'integer'; - } else if ($type === 'bool') { - $type = 'boolean'; - } + if (!static::$connection::schema()->hasColumn($tableName, $newColumn)) { + $newCol = $table->{$column['type']}($newColumn); - if (gettype($value) === 'NULL' && rtrim($column, '*') !== $column) { - $column .= '?'; - } + unset($column['type']); - return [$column, $type]; - } - - echo $key . " => " . $value . "\n"; - - if ( - (strtolower($key) === 'id' && gettype($value) === 'integer') || - (strpos(strtolower($key), '_id') !== false && gettype($value) === 'integer') - ) { - return [$key, 'bigIncrements']; - } + foreach ($column as $columnOptionName => $columnOptionValue) { + if (is_bool($columnOptionValue)) { + if ($columnOptionValue) { + $newCol->{$columnOptionName}(); + } + } else { + $newCol->{$columnOptionName}($columnOptionValue); + } + } + } + } + } - if ( - strpos(ltrim(strtolower(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $key)), '_'), '_at') !== false || - strpos(ltrim(strtolower(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $key)), '_'), '_date') !== false || - strpos(ltrim(strtolower(preg_replace('/[A-Z]([A-Z](?![a-z]))*/', '_$0', $key)), '_'), '_time') !== false || - strpos($key, 'timestamp') === 0 || - strpos($key, 'time') === 0 || - strpos($key, 'date') === 0 - ) { - return [$key, 'timestamp']; - } + if (count($columnsDiff) > 0) { + foreach ($columnsDiff as $changedColumn) { + $column = static::getColumnAttributes($columns[$changedColumn]); + $prevMigrationColumn = static::getColumnAttributes($lastMigration['columns'][$changedColumn] ?? []); + + if ($column['type'] === 'timestamp') { + continue; + } + + $newCol = $table->{$column['type']}( + $changedColumn, + ($column['type'] === 'string') ? $column['length'] : null + ); + + unset($column['type']); + + foreach ($column as $columnOptionName => $columnOptionValue) { + if ($columnOptionValue === $prevMigrationColumn[$columnOptionName]) { + continue; + } + + if ($columnOptionName === 'unique') { + if ($columnOptionValue) { + $newCol->unique()->change(); + } else { + $table->dropUnique("{$tableName}_{$changedColumn}_unique"); + } + + continue; + } + + if ($columnOptionName === 'index') { + if ($columnOptionValue) { + $newCol->index()->change(); + } else { + $table->dropIndex("{$tableName}_{$changedColumn}_index"); + } + + continue; + } + + // skipping this for now, primary + autoIncrement + // doesn't work well in the same run. They need to be + // run separately for some reason + // if ($columnOptionName === 'autoIncrement') { + + if ($columnOptionName === 'primary') { + if ($columnOptionValue) { + $newCol->primary()->change(); + } else { + $table->dropPrimary("{$tableName}_{$changedColumn}_primary"); + } + + continue; + } + + if ($columnOptionName === 'default') { + $newCol->default($columnOptionValue)->change(); + continue; + } + + if (is_bool($columnOptionValue)) { + + if ($columnOptionValue) { + $newCol->{$columnOptionName}()->change(); + } else { + $newCol->{$columnOptionName}(false)->change(); + } + } else { + $newCol->{$columnOptionName}($columnOptionValue)->change(); + } + } + + $newCol->change(); + } + } - if (gettype($value) === 'integer') { - return [$key, 'integer']; - } + if (count($removedColumns) > 0) { + foreach ($removedColumns as $removedColumn) { + if (static::$connection::schema()->hasColumn($tableName, $removedColumn)) { + $table->dropColumn($removedColumn); + } + } + } - if (gettype($value) === 'double') { - return [$key, 'float']; - } + if ($rememberToken !== ($lastMigration['remember_token'] ?? false)) { + if ($rememberToken && !static::$connection::schema()->hasColumn($tableName, 'remember_token')) { + $table->rememberToken(); + } else if (!$rememberToken && static::$connection::schema()->hasColumn($tableName, 'remember_token')) { + $table->dropRememberToken(); + } + } - if (gettype($value) === 'string') { - if (strpos($value, '{') === 0 || strpos($value, '[') === 0) { - return [$key, 'json']; - } + if ($softDeletes !== ($lastMigration['softDeletes'] ?? false)) { + if ($softDeletes && !static::$connection::schema()->hasColumn($tableName, 'deleted_at')) { + $table->softDeletes(); + } else if (!$softDeletes && static::$connection::schema()->hasColumn($tableName, 'deleted_at')) { + $table->dropSoftDeletes(); + } + } - if ($key === 'description' || $key === 'text' || strlen($value) > 150) { - return [$key, 'text']; + if ($timestamps !== ($lastMigration['timestamps'] ?? true)) { + if ($timestamps && !static::$connection::schema()->hasColumn($tableName, 'created_at')) { + $table->timestamps(); + } else if (!$timestamps && static::$connection::schema()->hasColumn($tableName, 'created_at')) { + $table->dropTimestamps(); + } + } + }); } - return [$key, 'string']; + storage()->copy( + $fileToMigrate, + StoragePath('database' . '/' . $tableName . '/' . tick()->format('YYYY_MM_DD_HHmmss[.yml]')), + ['recursive' => true] + ); + } catch (\Throwable $th) { + throw $th; } - if (gettype($value) === 'array') { - return [$key, 'enum']; - } + return true; + } + + /** + * Seed a database table from schema file + * + * @param string $fileToSeed The name of the schema file + * @return bool + */ + public static function seed(string $fileToSeed): bool + { + $data = Yaml::parseFile($fileToSeed); + return true; + } - if (gettype($value) === 'boolean') { - return [$key, 'boolean']; + /** + * Get all column attributes + */ + public static function getColumnAttributes($value) + { + $attributes = [ + 'type' => 'string', + 'length' => null, + 'nullable' => false, + 'default' => null, + 'unsigned' => false, + 'index' => false, + 'unique' => false, + 'primary' => false, + 'foreign' => false, + 'foreignTable' => null, + 'foreignColumn' => null, + 'onDelete' => null, + 'onUpdate' => null, + 'comment' => null, + 'autoIncrement' => false, + 'useCurrent' => false, + 'useCurrentOnUpdate' => false, + ]; + + if (is_string($value)) { + $attributes['type'] = $value; + } else if (is_array($value)) { + $attributes = array_merge($attributes, $value); } - return [$column, $type]; + return $attributes; + } + + /** + * Set the internal db connection + * @param mixed $connection + * @return void + */ + public static function setDbConnection($connection) + { + static::$connection = $connection; } } From 3ba90298e8ccc4ca775f5d3a1f7035f52b2e3e80 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Wed, 11 Dec 2024 22:14:14 +0000 Subject: [PATCH 03/29] feat: remove all deprecated stuff --- src/Controller.php | 49 +---------- src/Factory.php | 184 ------------------------------------------ src/globals/paths.php | 2 +- 3 files changed, 2 insertions(+), 233 deletions(-) delete mode 100755 src/Factory.php diff --git a/src/Controller.php b/src/Controller.php index 4635d3b..fffafa8 100755 --- a/src/Controller.php +++ b/src/Controller.php @@ -66,54 +66,7 @@ public function id() */ public function view(string $view, array $data = []) { - /// WILL REFACTOR IN NEXT VERSION - - if (is_object($data)) { - $data = (array) $data; - } - - if (ViewConfig('render')) { - if (ViewConfig('config')) { - call_user_func_array(ViewConfig('config'), [[ - 'views' => AppConfig('views.path'), - 'cache' => AppConfig('views.cachePath'), - ]]); - } - - return ViewConfig('render')($view, $data); - } - - $engine = ViewConfig('viewEngine'); - $className = strtolower(get_class(new $engine)); - - $fullName = explode('\\', $className); - $className = $fullName[count($fullName) - 1]; - - if (\Leaf\Config::getStatic("views.$className")) { - if (ViewConfig('config')) { - call_user_func_array(ViewConfig('config'), [[ - 'views' => AppConfig('views.path'), - 'cache' => AppConfig('views.cachePath'), - ]]); - } else { - \Leaf\Config::get("views.$className")->configure(AppConfig('views.path'), AppConfig('views.cachePath')); - } - - return \Leaf\Config::get("views.$className")->render($view, $data); - } - - $engine = new $engine($engine); - - if (ViewConfig('config')) { - call_user_func_array(ViewConfig('config'), [[ - 'views' => AppConfig('views.path'), - 'cache' => AppConfig('views.cachePath'), - ]]); - } else { - $engine->config(AppConfig('views.path'), AppConfig('views.cachePath')); - } - - return $engine->render($view, $data); + return view($view, $data); } /** diff --git a/src/Factory.php b/src/Factory.php deleted file mode 100755 index bd73261..0000000 --- a/src/Factory.php +++ /dev/null @@ -1,184 +0,0 @@ -faker = \Faker\Factory::create(); - } - - if (class_exists(\Illuminate\Support\Str::class)) { - $this->str = \Illuminate\Support\Str::class; - } - } - - /** - * Define the model's default state. - * - * @return array - */ - public function definition(): array - { - return []; - } - - /** - * Create a number of records based on definition - * - * @param int $number The number of records to create - * - * @return self - */ - public function create(int $number): Factory - { - $data = []; - - for ($i = 0; $i < $number; $i++) { - $data[] = $this->definition(); - } - - $this->data = $data; - - return $this; - } - - /** - * Create a relationship with another factory - * - * @param \Leaf\Factory $factory The instance of the factory to tie to - * @param array|string $primaryKey The primary key for that factory's table - * @throws \Exception - * @throws \Throwable - */ - public function has(Factory $factory, $primaryKey = null): Factory - { - if (count($this->data) === 0) { - $this->data[] = $this->definition(); - } - - $dataToOverride = []; - $model = $this->model ?? $this->getModelName(); - - if (!$primaryKey) { - $primaryKey = strtolower($this->getModelName() . '_id'); - $primaryKey = str_replace('\app\models\\', '', $primaryKey); - } - - if (is_array($primaryKey)) { - $dataToOverride = $primaryKey; - } else { - $key = explode('_', $primaryKey); - if (count($key) > 1) { - unset($key[0]); - } - $key = implode($key); - - $primaryKeyData = $this->data[\rand(0, count($this->data) - 1)][$key] ?? null; - $primaryKeyData = $primaryKeyData ?? $model::all()[\rand(0, count($model::all()) - 1)][$key]; - - $dataToOverride[$primaryKey] = $primaryKeyData; - } - - $factory->save($dataToOverride); - - return $this; - } - - /** - * Save created records in db - * - * @param array $override Override data to save - * - * @return true - * @throws \Exception - */ - public function save(array $override = []): bool - { - $model = $this->model ?? $this->getModelName(); - - if (count($this->data) === 0) { - $this->data[] = $this->definition(); - } - - foreach ($this->data as $item) { - $item = array_merge($item, $override); - - $model = new $model; - foreach ($item as $key => $value) { - $model->{$key} = $value; - } - $model->save(); - } - - return true; - } - - /** - * Return created records - * - * @param array|null $override Override data to save - * - * @return array - */ - public function get(array $override = null): array - { - if (count($this->data) === 0) { - $this->data[] = $this->definition(); - } - - if ($override) { - foreach ($this->data as $item) { - $item = array_merge($item, $override); - } - } - - return $this->data; - } - - /** - * Get the default model name - * @throws \Exception - */ - public function getModelName(): string - { - $class = get_class($this); - $modelClass = '\App\Models' . $this->str::studly(str_replace(['App\Database\Factories', 'Factory'], '', $class)); - - if (!class_exists($modelClass)) { - throw new \Exception('Couldn\'t retrieve model for ' . get_class($this) . '. Add a \$model attribute to fix this.'); - } - - return $modelClass; - } -} diff --git a/src/globals/paths.php b/src/globals/paths.php index 07156ac..26d25b8 100755 --- a/src/globals/paths.php +++ b/src/globals/paths.php @@ -45,7 +45,7 @@ function ControllersPath($path = ''): string if (!function_exists('DatabasePath')) { /** - * Database storage path + * Database path */ function DatabasePath($path = ''): string { From 190ef03271ff75866f931221d1f3dafeb1262abe Mon Sep 17 00:00:00 2001 From: mychidarko Date: Wed, 18 Dec 2024 16:30:37 +0000 Subject: [PATCH 04/29] chore: update readme --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 65b4ff4..bbebc8e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![Total Downloads](https://poser.pugx.org/leafs/mvc-core/downloads)](https://packagist.org/packages/leafs/mvc-core) [![License](https://poser.pugx.org/leafs/mvc-core/license)](https://packagist.org/packages/leafs/mvc-core) -This is the heart of Leaf MVC. It serves as a bridge between Leaf and the MVC file structure. It provides a ton of functionality that makes it easy to build a full-blown MVC application with Leaf. +Leaf MVC Core is the heart of Leaf MVC and serves as bridge between Leaf, modules and the MVC file structure. It provides a ton of extra functionality like extra globals, classes and methods that help with separation of concerns and building a full-blown MVC application with Leaf. ## 📦 Installation @@ -32,12 +32,9 @@ composer require leafs/mvc-core MVC Core comes with: - Controllers -- Api Controllers -- Database & Models -- Factories -- Models -- Schemas +- Database & Model functionalities - Tons of MVC and module globals +- Autoloading directory files Since you don't use this package on its own, the documentation is covered in the [Leaf MVC documentation](https://leafphp.dev/docs/mvc/). From 3664f6fc1dc5648bf5395b2efb7f7a7cd7ec5393 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Wed, 18 Dec 2024 16:30:51 +0000 Subject: [PATCH 05/29] fix: patch up csrf enabled --- src/Core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core.php b/src/Core.php index d2d2c2c..00fd014 100644 --- a/src/Core.php +++ b/src/Core.php @@ -50,7 +50,7 @@ public static function loadApplicationConfig() Config::getStatic('mvc.config.auth')['session'] ?? false ); - if ($csrfConfig['enabled'] ?? null !== null) { + if (($csrfConfig['enabled'] ?? null) !== null) { $csrfEnabled = $csrfConfig['enabled']; } From a88471e7ff641203e0f6fcf98c027b9054e5a476 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 22 Dec 2024 17:51:05 +0000 Subject: [PATCH 06/29] feat: update model relationships in schema --- src/Schema.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Schema.php b/src/Schema.php index d13976f..36ecf4d 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -53,6 +53,10 @@ public static function migrate(string $fileToMigrate): bool } foreach ($relationships as $model) { + if (strpos($model, 'App\Models') === false) { + $model = "App\Models\\$model"; + } + $table->foreignIdFor($model); } From 64ce23853288ae42531c28ef4828a7dafa677831 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 22 Dec 2024 17:51:47 +0000 Subject: [PATCH 07/29] fix: patch up schema db connection --- composer.json | 5 ++++- src/Database.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index d7ad5d1..f8362d2 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ ] }, "minimum-stability": "dev", - "prefer-stable": true, + "prefer-stable": true, "require": { "leafs/leaf": "*", "doctrine/dbal": "^3.2", @@ -40,5 +40,8 @@ "illuminate/database": "^8.75", "illuminate/events": "^8.75", "symfony/yaml": "^6.4" + }, + "require-dev": { + "fakerphp/faker": "^1.24" } } diff --git a/src/Database.php b/src/Database.php index 335da1d..27eddb0 100755 --- a/src/Database.php +++ b/src/Database.php @@ -38,7 +38,7 @@ public static function connect() static::$capsule->bootEloquent(); if (php_sapi_name() === 'cli') { - Schema::$capsule = static::$capsule; + Schema::setDbConnection(static::$capsule); } } From be76f7795698222aca13eaf14c662785d6552e06 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 22 Dec 2024 17:57:22 +0000 Subject: [PATCH 08/29] feat: automatically load all libraries if lib folder exists --- src/Core.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Core.php b/src/Core.php index 00fd014..055ff52 100644 --- a/src/Core.php +++ b/src/Core.php @@ -87,6 +87,10 @@ public static function loadApplicationConfig() call_user_func_array(ViewConfig('extend'), app()->template()); } } + + if (storage()->exists(LibPath())) { + static::loadLibs(); + } } } From ce4403c92c4cad9717b4e2e61fc344f433cb45ed Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 22 Dec 2024 18:13:55 +0000 Subject: [PATCH 09/29] feat: add db sync and app index support --- src/Core.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Core.php b/src/Core.php index 055ff52..8c37246 100644 --- a/src/Core.php +++ b/src/Core.php @@ -88,9 +88,17 @@ public static function loadApplicationConfig() } } + if (DatabaseConfig('sync')) { + \Leaf\Database::initDb(); + } + if (storage()->exists(LibPath())) { static::loadLibs(); } + + if (storage()->exists('app/index.php')) { + require 'app/index.php'; + } } } From 7d11c3719eb3577164748c237cf25e02a387e774 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 22 Dec 2024 22:50:15 +0000 Subject: [PATCH 10/29] fix: return null on missing config --- src/globals/config.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/globals/config.php b/src/globals/config.php index dcd443e..1fede03 100644 --- a/src/globals/config.php +++ b/src/globals/config.php @@ -95,5 +95,5 @@ function MailConfig($setting = null) function MvcConfig($appConfig, $setting = null) { $config = \Leaf\Config::getStatic("mvc.config.$appConfig"); - return !$setting ? $config : $config[$setting]; + return !$setting ? $config : ($config[$setting] ?? null); } From 3102bda01612451178626c37a23eb249c5cb8e75 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 30 Dec 2024 08:28:12 +0000 Subject: [PATCH 11/29] feat: add billing init --- src/Core.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Core.php b/src/Core.php index 8c37246..3ce017b 100644 --- a/src/Core.php +++ b/src/Core.php @@ -84,7 +84,7 @@ public static function loadApplicationConfig() } if (is_callable(ViewConfig('extend'))) { - call_user_func_array(ViewConfig('extend'), app()->template()); + call_user_func(ViewConfig('extend'), app()->template()); } } @@ -95,7 +95,15 @@ public static function loadApplicationConfig() if (storage()->exists(LibPath())) { static::loadLibs(); } - + + if ( + class_exists('Leaf\Billing\Stripe') || + class_exists('Leaf\Billing\PayStack') || + class_exists('Leaf\Billing\LemonSqueezy') + ) { + billing(Config::getStatic('mvc.config.billing')); + } + if (storage()->exists('app/index.php')) { require 'app/index.php'; } From e9f994869bb3065bb3d1b1419265a5f12f2c1d3d Mon Sep 17 00:00:00 2001 From: mychidarko Date: Fri, 10 Jan 2025 00:05:39 +0000 Subject: [PATCH 12/29] feat: switch leaf db to deferred connection --- src/Core.php | 6 +++--- src/Database.php | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Core.php b/src/Core.php index 3ce017b..1ba7ca9 100644 --- a/src/Core.php +++ b/src/Core.php @@ -88,9 +88,7 @@ public static function loadApplicationConfig() } } - if (DatabaseConfig('sync')) { - \Leaf\Database::initDb(); - } + \Leaf\Database::initDb(); if (storage()->exists(LibPath())) { static::loadLibs(); @@ -320,6 +318,8 @@ public static function loadConsole($externalCommands = []) */ public static function runApplication() { + static::loadApplicationConfig(); + $routePath = static::$paths['routes']; $routeFiles = glob("$routePath/*.php"); diff --git a/src/Database.php b/src/Database.php index 27eddb0..0083b93 100755 --- a/src/Database.php +++ b/src/Database.php @@ -45,8 +45,6 @@ public static function connect() /** * Create a Leaf Db connection using the the default connection * defined in the config/database.php file - * - * @return \PDO|null */ public static function initDb() { @@ -55,7 +53,7 @@ public static function initDb() $defaultConnection = $config['connections'][$config['default'] ?? 'mysql'] ?? []; if (!empty($defaultConnection)) { - return db()->connect([ + return db()->load([ 'dbUrl' => $defaultConnection['url'] ?? null, 'dbtype' => $defaultConnection['driver'] ?? 'mysql', 'charset' => $defaultConnection['charset'] ?? 'utf8mb4', From fad3bdbfe630c1522ae59988b5286feb5e5b71d5 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Fri, 10 Jan 2025 00:06:11 +0000 Subject: [PATCH 13/29] feat: add seeding support --- src/Schema.php | 109 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 4 deletions(-) diff --git a/src/Schema.php b/src/Schema.php index 36ecf4d..e6e69f7 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -29,10 +29,6 @@ public static function migrate(string $fileToMigrate): bool $data = Yaml::parseFile($fileToMigrate); $tableName = rtrim(path($fileToMigrate)->basename(), '.yml'); - if ($data['truncate'] ?? false) { - static::$connection::schema()->dropIfExists($tableName); - } - try { if (!static::$connection::schema()->hasTable($tableName)) { if (storage()->exists(StoragePath("database/$tableName"))) { @@ -285,6 +281,111 @@ public static function migrate(string $fileToMigrate): bool public static function seed(string $fileToSeed): bool { $data = Yaml::parseFile($fileToSeed); + $tableName = rtrim(path($fileToSeed)->basename(), '.yml'); + + $seeds = $data['seeds'] ?? []; + $count = $seeds['count'] ?? 1; + $seedsData = $seeds['data'] ?? []; + + $timestamps = $data['timestamps'] ?? true; + $softDeletes = $data['softDeletes'] ?? false; + $rememberToken = $data['remember_token'] ?? false; + + $finalDataToSeed = []; + + if ($seeds['truncate'] ?? false) { + static::$connection::table($tableName)->truncate(); + } + + if (is_array($seedsData[0] ?? null)) { + $finalDataToSeed = $seedsData; + } else { + for ($i = 0; $i < $count; $i++) { + $parsedData = []; + + foreach ($seedsData as $key => $value) { + $valueArray = explode('.', $value); + + if ($valueArray[0] === '@faker') { + $localFakerInstance = \Faker\Factory::create(); + + foreach ($valueArray as $index => $fakerMethod) { + if ($index === 0) { + continue; + } + + if (strpos($fakerMethod, ':') !== false) { + $fakerMethod = explode(':', $fakerMethod); + $localFakerInstance = $localFakerInstance->{$fakerMethod[0]}($fakerMethod[1]); + } else { + $localFakerInstance = $localFakerInstance->{$fakerMethod}(); + } + } + + $parsedData[$key] = $localFakerInstance; + + continue; + } + + if ($valueArray[0] === '@tick') { + $localTickInstance = tick(); + + foreach ($valueArray as $index => $tickMethod) { + if ($index === 0) { + continue; + } + + if (strpos($tickMethod, ':') !== false) { + $tickMethod = explode(':', $tickMethod); + $localTickInstance = $localTickInstance->{$tickMethod[0]}($tickMethod[1]); + } else { + $localTickInstance = $localTickInstance->{$tickMethod}(); + } + } + + $parsedData[$key] = $localTickInstance; + + continue; + } + + if (strpos($value, '@randomString') === 0) { + $value = explode(':', $value); + $parsedData[$key] = \Illuminate\Support\Str::random($value[1] ?? 10); + + continue; + } + + if (strpos($value, '@hash') === 0) { + $value = explode(':', $value); + $parsedData[$key] = \Leaf\Helpers\Password::hash($value[1] ?? 'password'); + + continue; + } + + $parsedData[$key] = $value; + } + + $finalDataToSeed[] = $parsedData; + } + } + + foreach ($finalDataToSeed as $itemToSeed) { + if ($rememberToken) { + $itemToSeed['remember_token'] = \Illuminate\Support\Str::random(10); + } + + if ($softDeletes) { + $itemToSeed['deleted_at'] = null; + } + + if ($timestamps) { + $itemToSeed['created_at'] = tick()->format('YYYY-MM-DD HH:mm:ss'); + $itemToSeed['updated_at'] = tick()->format('YYYY-MM-DD HH:mm:ss'); + } + + static::$connection::table($tableName)->insert($itemToSeed); + } + return true; } From ac8efe87293613bb4ddab78db82193a8a3b2f755 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Fri, 10 Jan 2025 09:47:58 +0000 Subject: [PATCH 14/29] feat: add support for db reset --- src/Schema.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Schema.php b/src/Schema.php index e6e69f7..3c9d7f5 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -389,6 +389,24 @@ public static function seed(string $fileToSeed): bool return true; } + /** + * Reset a database table + */ + public static function reset(string $fileToReset): bool + { + $tableName = rtrim(path($fileToReset)->basename(), '.yml'); + + if (static::$connection::schema()->hasTable($tableName)) { + static::$connection::schema()->dropIfExists($tableName); + + if (storage()->exists(StoragePath("database/$tableName"))) { + storage()->delete(StoragePath("database/$tableName")); + } + } + + return static::migrate($fileToReset); + } + /** * Get all column attributes */ From c1f6b031ff1648f43e0467ca6193b1160adcd919 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Fri, 10 Jan 2025 15:55:29 +0000 Subject: [PATCH 15/29] feat: add rollback support --- src/Schema.php | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/Schema.php b/src/Schema.php index 3c9d7f5..18e691f 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -407,6 +407,42 @@ public static function reset(string $fileToReset): bool return static::migrate($fileToReset); } + /** + * Rollback db to a previous state + */ + public static function rollback(string $fileToRollback, int $step = 1): bool + { + $tableName = rtrim(path($fileToRollback)->basename(), '.yml'); + + if (!storage()->exists(StoragePath("database/$tableName"))) { + return false; + } + + $files = glob(StoragePath("database/$tableName/*.yml")); + + if (count($files) === 0) { + return false; + } + + $migrationStep = count($files) - $step; + $currentFileToRollback = $files[$migrationStep] ?? null; + + if (!$currentFileToRollback) { + return false; + } + + $files = array_reverse($files); + + for ($i = 0; $i < ($step - 1); $i++) { + storage()->delete($files[$i]); + } + + storage()->rename($fileToRollback, StoragePath('database' . '/' . $tableName . '/' . tick()->format('YYYY_MM_DD_HHmmss[.yml]'))); + storage()->rename($currentFileToRollback, $fileToRollback); + + return static::migrate($fileToRollback); + } + /** * Get all column attributes */ From 878d42aa9e1822b7b9a32fcb0e67da5ff24e1427 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 20 Jan 2025 13:03:53 +0000 Subject: [PATCH 16/29] chore: update version --- src/Core.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core.php b/src/Core.php index 1ba7ca9..2d6cfe4 100644 --- a/src/Core.php +++ b/src/Core.php @@ -291,7 +291,7 @@ public static function loadConsole($externalCommands = []) \Leaf\Database::connect(); - $console = new \Aloe\Console('v3.8.0'); + $console = new \Aloe\Console('v4.x-ALPHA'); if (\Leaf\FS\Directory::exists(static::$paths['commands'])) { $consolePath = static::$paths['commands']; From 615612c51fdca8e90be8ec7278d4bbdb54ed8970 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 20 Jan 2025 23:18:40 +0000 Subject: [PATCH 17/29] fix: install leaf db --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index f8362d2..a4ae266 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,8 @@ "vlucas/phpdotenv": "^5.4", "illuminate/database": "^8.75", "illuminate/events": "^8.75", - "symfony/yaml": "^6.4" + "symfony/yaml": "^6.4", + "leafs/db": "^2.3" }, "require-dev": { "fakerphp/faker": "^1.24" From bea49635f1e2d0115a41b86a10ec40dd0644b758 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sat, 25 Jan 2025 23:07:19 +0000 Subject: [PATCH 18/29] fix: patch up mail publish not working --- mail.php | 113 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 mail.php diff --git a/mail.php b/mail.php new file mode 100644 index 0000000..18ba231 --- /dev/null +++ b/mail.php @@ -0,0 +1,113 @@ + _env('MAIL_HOST', 'smtp.mailtrap.io'), + + /* + |-------------------------------------------------------------------------- + | SMTP Host Port + |-------------------------------------------------------------------------- + | + | Here you may specify the port used for your application. + | + */ + 'port' => _env('MAIL_PORT', 2525), + + /* + |-------------------------------------------------------------------------- + | Keep Alive + |-------------------------------------------------------------------------- + | + | This config is used to keep the connection to your mail server alive. + | This is useful if you are sending multiple emails. It takes in a boolean. + | + */ + 'keepAlive' => true, + + /* + |-------------------------------------------------------------------------- + | Debug mode + |-------------------------------------------------------------------------- + | + | When this option is enabled, the mailer will log all the communication + | with the SMTP server. This can come in handy when you're trying to + | debug an application. + | + */ + 'debug' => _env('MAIL_DEBUG', 'SERVER'), + + /* + |-------------------------------------------------------------------------- + | Mail encryption + |-------------------------------------------------------------------------- + | + | Here you may specify the encryption protocol that should be used when + | the application send mail messages. A sensible default using the + | transport layer security protocol should provide great security. + | + */ + 'security' => _env('MAIL_ENCRYPTION', 'STARTTLS'), + + /* + |-------------------------------------------------------------------------- + | Server Auth + |-------------------------------------------------------------------------- + | + | This config handles the authentication details for your mailer. + | It supports authentication with username and password and also + | OAuth authentication. + | + | For OAuth authentication, you will need to add an OAuth + | provider like league/oauth2-google to your project. + | + | An example OAuth config is shown below: + | + | use League\OAuth2\Client\Provider\Google; + | use PHPMailer\PHPMailer\OAuth; + | + | 'auth' => new OAuth( + | [ + | 'userName' => 'mail@gmail.com', + | 'clientSecret' => 'CLIENT_SECRET', + | 'clientId' => 'CLIENT_ID', + | 'refreshToken' => 'GMAIL_REFRESH_TOKEN', + | 'provider' => new Google( + | [ + | 'clientId' => 'CLIENT_ID', + | 'clientSecret' => 'CLIENT_SECRET', + | ] + | ), + | ] + |) + */ + 'auth' => [ + 'username' => _env('MAIL_USERNAME'), + 'password' => _env('MAIL_PASSWORD'), + ], + + /* + |-------------------------------------------------------------------------- + | Default addresses + |-------------------------------------------------------------------------- + | + | This config is used to set default values for the + | `recipientEmail`, `recipientName`, + | `senderEmail`, `senderName`, + | `replyToName`, and `replyToEmail` of your emails. + | + */ + 'defaults' => [ + 'senderName' => _env('MAIL_SENDER_NAME'), + 'senderEmail' => _env('MAIL_SENDER_EMAIL'), + 'replyToName' => _env('MAIL_REPLY_TO_NAME'), + 'replyToEmail' => _env('MAIL_REPLY_TO_EMAIL'), + ], +]; From 03ab09ca2f151d8e0115cb0ae67432d182626996 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sat, 25 Jan 2025 23:09:55 +0000 Subject: [PATCH 19/29] chore: remove unused files --- mail.php | 113 ------------------------------------------------------- 1 file changed, 113 deletions(-) delete mode 100644 mail.php diff --git a/mail.php b/mail.php deleted file mode 100644 index 18ba231..0000000 --- a/mail.php +++ /dev/null @@ -1,113 +0,0 @@ - _env('MAIL_HOST', 'smtp.mailtrap.io'), - - /* - |-------------------------------------------------------------------------- - | SMTP Host Port - |-------------------------------------------------------------------------- - | - | Here you may specify the port used for your application. - | - */ - 'port' => _env('MAIL_PORT', 2525), - - /* - |-------------------------------------------------------------------------- - | Keep Alive - |-------------------------------------------------------------------------- - | - | This config is used to keep the connection to your mail server alive. - | This is useful if you are sending multiple emails. It takes in a boolean. - | - */ - 'keepAlive' => true, - - /* - |-------------------------------------------------------------------------- - | Debug mode - |-------------------------------------------------------------------------- - | - | When this option is enabled, the mailer will log all the communication - | with the SMTP server. This can come in handy when you're trying to - | debug an application. - | - */ - 'debug' => _env('MAIL_DEBUG', 'SERVER'), - - /* - |-------------------------------------------------------------------------- - | Mail encryption - |-------------------------------------------------------------------------- - | - | Here you may specify the encryption protocol that should be used when - | the application send mail messages. A sensible default using the - | transport layer security protocol should provide great security. - | - */ - 'security' => _env('MAIL_ENCRYPTION', 'STARTTLS'), - - /* - |-------------------------------------------------------------------------- - | Server Auth - |-------------------------------------------------------------------------- - | - | This config handles the authentication details for your mailer. - | It supports authentication with username and password and also - | OAuth authentication. - | - | For OAuth authentication, you will need to add an OAuth - | provider like league/oauth2-google to your project. - | - | An example OAuth config is shown below: - | - | use League\OAuth2\Client\Provider\Google; - | use PHPMailer\PHPMailer\OAuth; - | - | 'auth' => new OAuth( - | [ - | 'userName' => 'mail@gmail.com', - | 'clientSecret' => 'CLIENT_SECRET', - | 'clientId' => 'CLIENT_ID', - | 'refreshToken' => 'GMAIL_REFRESH_TOKEN', - | 'provider' => new Google( - | [ - | 'clientId' => 'CLIENT_ID', - | 'clientSecret' => 'CLIENT_SECRET', - | ] - | ), - | ] - |) - */ - 'auth' => [ - 'username' => _env('MAIL_USERNAME'), - 'password' => _env('MAIL_PASSWORD'), - ], - - /* - |-------------------------------------------------------------------------- - | Default addresses - |-------------------------------------------------------------------------- - | - | This config is used to set default values for the - | `recipientEmail`, `recipientName`, - | `senderEmail`, `senderName`, - | `replyToName`, and `replyToEmail` of your emails. - | - */ - 'defaults' => [ - 'senderName' => _env('MAIL_SENDER_NAME'), - 'senderEmail' => _env('MAIL_SENDER_EMAIL'), - 'replyToName' => _env('MAIL_REPLY_TO_NAME'), - 'replyToEmail' => _env('MAIL_REPLY_TO_EMAIL'), - ], -]; From 4b2700f1b7b5f62e5bb6718b15d527696a5957d8 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sat, 8 Feb 2025 18:56:22 +0000 Subject: [PATCH 20/29] feat: switch to connect to match new API --- src/Database.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Database.php b/src/Database.php index 0083b93..f1a6fca 100755 --- a/src/Database.php +++ b/src/Database.php @@ -53,7 +53,7 @@ public static function initDb() $defaultConnection = $config['connections'][$config['default'] ?? 'mysql'] ?? []; if (!empty($defaultConnection)) { - return db()->load([ + return db()->connect([ 'dbUrl' => $defaultConnection['url'] ?? null, 'dbtype' => $defaultConnection['driver'] ?? 'mysql', 'charset' => $defaultConnection['charset'] ?? 'utf8mb4', From ebd5c76aaa958e7b974d25e6d61b79a6d96ccbeb Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 10 Feb 2025 14:33:09 +0000 Subject: [PATCH 21/29] feat: change config loading to increase performance --- src/Core.php | 234 ++++++++++++++++++++--------------------- src/globals/config.php | 2 +- 2 files changed, 116 insertions(+), 120 deletions(-) diff --git a/src/Core.php b/src/Core.php index 2d6cfe4..718441d 100644 --- a/src/Core.php +++ b/src/Core.php @@ -27,81 +27,19 @@ public static function loadApplicationConfig() { static::loadConfig(); - if (class_exists('Leaf\Auth')) { - auth()->config(Config::getStatic('mvc.config.auth')); - } - - if (class_exists('Leaf\Mail')) { - mailer()->connect(Config::getStatic('mvc.config.mail')); - } - if (php_sapi_name() !== 'cli') { - app()->config(Config::getStatic('mvc.config.app')); - - if (class_exists('Leaf\Http\Cors')) { - app()->cors(Config::getStatic('mvc.config.cors')); - } - - if (class_exists('Leaf\Anchor\CSRF')) { - $csrfConfig = Config::getStatic('mvc.config.csrf'); - - $csrfEnabled = ( - $csrfConfig && - Config::getStatic('mvc.config.auth')['session'] ?? false - ); - - if (($csrfConfig['enabled'] ?? null) !== null) { - $csrfEnabled = $csrfConfig['enabled']; - } - - if ($csrfEnabled) { - app()->csrf($csrfConfig); - } - } - if (class_exists('Leaf\Vite')) { \Leaf\Vite::config('assets', PublicPath('build')); \Leaf\Vite::config('build', 'public/build'); \Leaf\Vite::config('hotFile', 'public/hot'); } - if (ViewConfig('viewEngine')) { - Config::attachView(ViewConfig('viewEngine'), 'template'); - - if (ViewConfig('config')) { - call_user_func_array(ViewConfig('config'), [ - app()->template(), - [ - 'views' => AppConfig('views.path'), - 'cache' => AppConfig('views.cachePath'), - ] - ]); - } else if (method_exists(app()->template(), 'configure')) { - app()->template()->configure([ - 'views' => AppConfig('views.path'), - 'cache' => AppConfig('views.cachePath'), - ]); - } - - if (is_callable(ViewConfig('extend'))) { - call_user_func(ViewConfig('extend'), app()->template()); - } - } - \Leaf\Database::initDb(); if (storage()->exists(LibPath())) { static::loadLibs(); } - if ( - class_exists('Leaf\Billing\Stripe') || - class_exists('Leaf\Billing\PayStack') || - class_exists('Leaf\Billing\LemonSqueezy') - ) { - billing(Config::getStatic('mvc.config.billing')); - } - if (storage()->exists('app/index.php')) { require 'app/index.php'; } @@ -129,47 +67,6 @@ protected static function loadConfig() 'views.path' => ViewsPath(null, false), 'views.cachePath' => StoragePath('framework/views') ], - 'auth' => [ - 'db.table' => 'users', - 'id.key' => 'id', - 'timestamps' => true, - 'timestamps.format' => 'YYYY-MM-DD HH:mm:ss', - 'unique' => ['email'], - 'hidden' => ['field.id', 'field.password'], - 'session' => _env('AUTH_SESSION', true), - 'session.lifetime' => 60 * 60 * 24, - 'session.cookie' => ['secure' => false, 'httponly' => true, 'samesite' => 'lax'], - 'token.lifetime' => 60 * 60 * 24 * 365, - 'token.secret' => _env('AUTH_TOKEN_SECRET', '@leaf$MVC*JWT#AUTH.Secret'), - 'messages.loginParamsError' => 'Incorrect credentials!', - 'messages.loginPasswordError' => 'Password is incorrect!', - 'password.key' => 'password', - 'password.encode' => function ($password) { - return \Leaf\Helpers\Password::hash($password); - }, - 'password.verify' => function ($password, $hashedPassword) { - return \Leaf\Helpers\Password::verify($password, $hashedPassword); - }, - ], - 'cors' => [ - 'origin' => _env('CORS_ALLOWED_ORIGINS', '*'), - 'methods' => _env('CORS_ALLOWED_METHODS', 'GET,HEAD,PUT,PATCH,POST,DELETE'), - 'allowedHeaders' => _env('CORS_ALLOWED_HEADERS', '*'), - 'exposedHeaders' => _env('CORS_EXPOSED_HEADERS', ''), - 'credentials' => false, - 'maxAge' => null, - 'preflightContinue' => false, - 'optionsSuccessStatus' => 204, - ], - 'csrf' => [ - 'secret' => _env('APP_KEY', '@nkor_leaf$0Secret!!'), - 'secretKey' => 'X-Leaf-CSRF-Token', - 'except' => [], - 'methods' => ['POST', 'PUT', 'PATCH', 'DELETE'], - 'messages.tokenNotFound' => 'Token not found.', - 'messages.tokenInvalid' => 'Invalid token.', - 'onError' => null, - ], 'database' => [ 'default' => _env('DB_CONNECTION', 'mysql'), 'connections' => [ @@ -229,13 +126,114 @@ protected static function loadConfig() ], 'view' => [ 'viewEngine' => \Leaf\Blade::class, - 'config' => function ($engine, $config) { - $engine->configure($config['views'], $config['cache']); + 'config' => function ($engine, $viewConfig) { + $engine->configure($viewConfig['views'], $viewConfig['cache']); }, 'render' => null, 'extend' => null, ], - 'mail' => [ + ]; + + if (storage()->exists($configPath = static::$paths['config'])) { + foreach (glob("$configPath/*.php") as $configFile) { + $config[basename($configFile, '.php')] = require $configFile; + } + } + + app()->config($config['app']); + + if ($config['view']['viewEngine']) { + Config::attachView($config['view']['viewEngine'], 'template'); + + if ($config['view']['config']) { + call_user_func_array($config['view']['config'], [ + app()->template(), + [ + 'views' => $config['app']['views.path'], + 'cache' => $config['app']['views.cachePath'], + ] + ]); + } else if (method_exists(app()->template(), 'configure')) { + app()->template()->configure([ + 'views' => $config['app']['views.path'], + 'cache' => $config['app']['views.cachePath'], + ]); + } + + if (is_callable($config['view']['extend'])) { + call_user_func($config['view']['extend'], app()->template()); + } + } + + if (class_exists('Leaf\Auth')) { + $config['auth'] = [ + 'db.table' => 'users', + 'id.key' => 'id', + 'timestamps' => true, + 'timestamps.format' => 'YYYY-MM-DD HH:mm:ss', + 'unique' => ['email'], + 'hidden' => ['field.id', 'field.password'], + 'session' => _env('AUTH_SESSION', true), + 'session.lifetime' => 60 * 60 * 24, + 'session.cookie' => ['secure' => false, 'httponly' => true, 'samesite' => 'lax'], + 'token.lifetime' => 60 * 60 * 24 * 365, + 'token.secret' => _env('AUTH_TOKEN_SECRET', '@leaf$MVC*JWT#AUTH.Secret'), + 'messages.loginParamsError' => 'Incorrect credentials!', + 'messages.loginPasswordError' => 'Password is incorrect!', + 'password.key' => 'password', + 'password.encode' => function ($password) { + return \Leaf\Helpers\Password::hash($password); + }, + 'password.verify' => function ($password, $hashedPassword) { + return \Leaf\Helpers\Password::verify($password, $hashedPassword); + }, + ]; + + auth()->config($config['auth']); + } + + if (class_exists('Leaf\Http\Cors')) { + $config['cors'] = [ + 'origin' => _env('CORS_ALLOWED_ORIGINS', '*'), + 'methods' => _env('CORS_ALLOWED_METHODS', 'GET,HEAD,PUT,PATCH,POST,DELETE'), + 'allowedHeaders' => _env('CORS_ALLOWED_HEADERS', '*'), + 'exposedHeaders' => _env('CORS_EXPOSED_HEADERS', ''), + 'credentials' => false, + 'maxAge' => null, + 'preflightContinue' => false, + 'optionsSuccessStatus' => 204, + ]; + + app()->cors($config['cors']); + } + + if (class_exists('Leaf\Anchor\CSRF')) { + $config['csrf'] = [ + 'secret' => _env('APP_KEY', '@nkor_leaf$0Secret!!'), + 'secretKey' => 'X-Leaf-CSRF-Token', + 'except' => [], + 'methods' => ['POST', 'PUT', 'PATCH', 'DELETE'], + 'messages.tokenNotFound' => 'Token not found.', + 'messages.tokenInvalid' => 'Invalid token.', + 'onError' => null, + ]; + + $csrfEnabled = ( + $config['csrf'] && + Config::getStatic('mvc.config.auth')['session'] ?? false + ); + + if (($config['csrf']['enabled'] ?? null) !== null) { + $csrfEnabled = $config['csrf']['enabled']; + } + + if ($csrfEnabled) { + app()->csrf($config['csrf']); + } + } + + if (class_exists('Leaf\Mail')) { + $config['mail'] = [ 'host' => _env('MAIL_HOST', 'smtp.mailtrap.io'), 'port' => _env('MAIL_PORT', 2525), 'keepAlive' => true, @@ -251,22 +249,20 @@ protected static function loadConfig() 'replyToName' => _env('MAIL_REPLY_TO_NAME'), 'replyToEmail' => _env('MAIL_REPLY_TO_EMAIL'), ], - ], - ]; + ]; - foreach ($config as $configName => $config) { - \Leaf\Config::set("mvc.config.$configName", $config); + mailer()->connect($config['mail']); } - $configPath = static::$paths['config']; - $configFiles = glob("$configPath/*.php"); - - foreach ($configFiles as $configFile) { - $configName = basename($configFile, '.php'); - $config = require $configFile; - - \Leaf\Config::set("mvc.config.$configName", $config); + if ( + class_exists('Leaf\Billing\Stripe') || + class_exists('Leaf\Billing\PayStack') || + class_exists('Leaf\Billing\LemonSqueezy') + ) { + billing($config['billing']); } + + Config::set('mvc.config', $config); } /** @@ -291,7 +287,7 @@ public static function loadConsole($externalCommands = []) \Leaf\Database::connect(); - $console = new \Aloe\Console('v4.x-ALPHA'); + $console = new \Aloe\Console('v4.x-BETA'); if (\Leaf\FS\Directory::exists(static::$paths['commands'])) { $consolePath = static::$paths['commands']; diff --git a/src/globals/config.php b/src/globals/config.php index 1fede03..eeea0ab 100644 --- a/src/globals/config.php +++ b/src/globals/config.php @@ -94,6 +94,6 @@ function MailConfig($setting = null) */ function MvcConfig($appConfig, $setting = null) { - $config = \Leaf\Config::getStatic("mvc.config.$appConfig"); + $config = \Leaf\Config::getStatic('mvc.config')[$appConfig] ?? null; return !$setting ? $config : ($config[$setting] ?? null); } From e2d8436341eebd57e1bd35d8be7cad06eac9b392 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Mon, 10 Feb 2025 14:46:24 +0000 Subject: [PATCH 22/29] fix: update to use new config API --- src/Core.php | 2 +- src/Database.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core.php b/src/Core.php index 718441d..89be9a5 100644 --- a/src/Core.php +++ b/src/Core.php @@ -220,7 +220,7 @@ protected static function loadConfig() $csrfEnabled = ( $config['csrf'] && - Config::getStatic('mvc.config.auth')['session'] ?? false + $config['auth']['session'] ?? false ); if (($config['csrf']['enabled'] ?? null) !== null) { diff --git a/src/Database.php b/src/Database.php index f1a6fca..b263c09 100755 --- a/src/Database.php +++ b/src/Database.php @@ -23,7 +23,7 @@ public static function connect() { static::$capsule = new Manager; - $config = Config::getStatic('mvc.config.database'); + $config = Config::getStatic('mvc.config')['database'] ?? []; $connections = $config['connections'] ?? []; foreach ($connections as $name => $connection) { @@ -49,7 +49,7 @@ public static function connect() public static function initDb() { if (function_exists('db')) { - $config = Config::getStatic('mvc.config.database'); + $config = Config::getStatic('mvc.config')['database'] ?? []; $defaultConnection = $config['connections'][$config['default'] ?? 'mysql'] ?? []; if (!empty($defaultConnection)) { From f1eeaeda5fb6a6d85bc5bea9de6667016eed97a0 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Tue, 18 Feb 2025 19:37:25 +0000 Subject: [PATCH 23/29] feat: automatically set up queue config and commands --- src/Core.php | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Core.php b/src/Core.php index 89be9a5..98c168f 100644 --- a/src/Core.php +++ b/src/Core.php @@ -27,6 +27,8 @@ public static function loadApplicationConfig() { static::loadConfig(); + \Leaf\Database::initDb(); + if (php_sapi_name() !== 'cli') { if (class_exists('Leaf\Vite')) { \Leaf\Vite::config('assets', PublicPath('build')); @@ -34,8 +36,6 @@ public static function loadApplicationConfig() \Leaf\Vite::config('hotFile', 'public/hot'); } - \Leaf\Database::initDb(); - if (storage()->exists(LibPath())) { static::loadLibs(); } @@ -254,6 +254,24 @@ protected static function loadConfig() mailer()->connect($config['mail']); } + if (class_exists('Leaf\Queue')) { + $config['queue'] = [ + 'default' => _env('QUEUE_CONNECTION', 'database'), + 'connections' => [ + 'redis' => [ + 'driver' => 'redis', + 'connection' => _env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => _env('REDIS_QUEUE', 'default'), + ], + 'database' => [ + 'driver' => 'database', + 'connection' => _env('DB_QUEUE_CONNECTION', 'default'), + 'table' => _env('DB_QUEUE_TABLE', 'leaf_php_jobs'), + ], + ], + ]; + } + if ( class_exists('Leaf\Billing\Stripe') || class_exists('Leaf\Billing\PayStack') || @@ -302,6 +320,10 @@ public static function loadConsole($externalCommands = []) } } + if (class_exists('Leaf\Queue')) { + $externalCommands[] = \Leaf\Queue::commands(); + } + foreach ($externalCommands as $command) { $console->register($command); } From 96d559ff0a48386ed202f87b244e88b726e2e8eb Mon Sep 17 00:00:00 2001 From: mychidarko Date: Wed, 19 Feb 2025 20:29:51 +0000 Subject: [PATCH 24/29] chore: update db version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index a4ae266..6ed9ee3 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "illuminate/database": "^8.75", "illuminate/events": "^8.75", "symfony/yaml": "^6.4", - "leafs/db": "^2.3" + "leafs/db": "v4.x-dev" }, "require-dev": { "fakerphp/faker": "^1.24" From 15ded186e4d591660b9a5c17af38c8880c42d5ca Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 20 Feb 2025 11:22:49 +0000 Subject: [PATCH 25/29] feat: automatically setup redis if available --- src/Core.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Core.php b/src/Core.php index 98c168f..2600567 100644 --- a/src/Core.php +++ b/src/Core.php @@ -272,6 +272,24 @@ protected static function loadConfig() ]; } + if (class_exists('Leaf\Redis')) { + $config['redis'] = [ + 'port' => 6379, + 'scheme' => 'tcp', + 'password' => null, + 'host' => '127.0.0.1', + 'session' => false, + 'session.savePath' => null, + 'session.saveOptions' => [], + 'connection.timeout' => 0.0, + 'connection.reserved' => null, + 'connection.retryInterval' => 0, + 'connection.readTimeout' => 0.0, + ]; + + redis()->connect($config['redis']); + } + if ( class_exists('Leaf\Billing\Stripe') || class_exists('Leaf\Billing\PayStack') || From c8639af361f27f4bf32a2a708df7a8017bbc89d7 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Thu, 20 Feb 2025 11:23:33 +0000 Subject: [PATCH 26/29] feat: sync db to eloquent config --- src/Database.php | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/Database.php b/src/Database.php index b263c09..eacc2e1 100755 --- a/src/Database.php +++ b/src/Database.php @@ -49,24 +49,26 @@ public static function connect() public static function initDb() { if (function_exists('db')) { + $connections = []; $config = Config::getStatic('mvc.config')['database'] ?? []; - $defaultConnection = $config['connections'][$config['default'] ?? 'mysql'] ?? []; - if (!empty($defaultConnection)) { - return db()->connect([ - 'dbUrl' => $defaultConnection['url'] ?? null, - 'dbtype' => $defaultConnection['driver'] ?? 'mysql', - 'charset' => $defaultConnection['charset'] ?? 'utf8mb4', - 'port' => $defaultConnection['port'] ?? '3306', - 'host' => $defaultConnection['host'] ?? '127.0.0.1', - 'username' => $defaultConnection['username'] ?? 'root', - 'password' => $defaultConnection['password'] ?? '', - 'dbname' => $defaultConnection['database'] ?? 'leaf_db', - 'collation' => $defaultConnection['collation'] ?? 'utf8mb4_unicode_ci', - 'prefix' => $defaultConnection['prefix'] ?? '', - 'unix_socket' => $defaultConnection['unix_socket'] ?? '', - ]); + foreach ($config['connections'] as $key => $connection) { + $connections[$key] = [ + 'dbUrl' => $connection['url'] ?? null, + 'dbtype' => $connection['driver'], + 'charset' => $connection['charset'] ?? null, + 'port' => $connection['port'] ?? null, + 'host' => $connection['host'] ?? null, + 'username' => $connection['username'] ?? null, + 'password' => $connection['password'] ?? null, + 'dbname' => $connection['database'], + 'collation' => $connection['collation'] ?? 'utf8mb4_unicode_ci', + 'prefix' => $connection['prefix'] ?? '', + 'unix_socket' => $connection['unix_socket'] ?? '', + ]; } + + db()->addConnections($connections, $config['default']); } return null; From 22198c78ef3af5c29c3ebb805876f16454e9f384 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Sun, 23 Feb 2025 13:27:53 +0000 Subject: [PATCH 27/29] feat: add support for web/API modes --- src/Core.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Core.php b/src/Core.php index 2600567..c2975a5 100644 --- a/src/Core.php +++ b/src/Core.php @@ -11,6 +11,8 @@ class Core { protected static $paths; + protected static $mode = 'web'; + /** * Return application paths * @return array @@ -314,6 +316,18 @@ public static function loadLibs() } } + /** + * Set mode for Leaf MVC: API or Web + */ + public static function mode(?string $mode = null) + { + if ($mode === null) { + return static::$mode; + } + + static::$mode = $mode; + } + /** * Load Aloe console and user defined commands */ From 508a86046c17b376b7e010a4c1f638a24b90208f Mon Sep 17 00:00:00 2001 From: mychidarko Date: Fri, 28 Feb 2025 17:20:54 +0000 Subject: [PATCH 28/29] chore: update version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6ed9ee3..5601fc2 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,7 @@ "illuminate/database": "^8.75", "illuminate/events": "^8.75", "symfony/yaml": "^6.4", - "leafs/db": "v4.x-dev" + "leafs/db": "*" }, "require-dev": { "fakerphp/faker": "^1.24" From feee645e08b3199334af6d24cae354882e744bf4 Mon Sep 17 00:00:00 2001 From: mychidarko Date: Wed, 5 Mar 2025 11:56:45 +0000 Subject: [PATCH 29/29] feat: add special cases to schema diff --- src/Schema.php | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Schema.php b/src/Schema.php index 18e691f..3cf71e2 100644 --- a/src/Schema.php +++ b/src/Schema.php @@ -110,6 +110,16 @@ public static function migrate(string $fileToMigrate): bool } } + if ($relationships !== ($lastMigration['relationships'] ?? [])) { + foreach ($relationships as $model) { + if (strpos($model, 'App\Models') === false) { + $model = "App\Models\\$model"; + } + + $table->foreignIdFor($model); + } + } + $columnsDiff = []; $staticColumns = []; $removedColumns = []; @@ -132,7 +142,17 @@ public static function migrate(string $fileToMigrate): bool $column = static::getColumnAttributes($columns[$newColumn]); if (!static::$connection::schema()->hasColumn($tableName, $newColumn)) { - $newCol = $table->{$column['type']}($newColumn); + // [TODO] Add more special cases + if ($column['type'] === 'string') { + $newCol = $table->string( + $newColumn, + $column['length'] ?? 255 + ); + + unset($column['length']); + } else { + $newCol = $table->{$column['type']}($newColumn); + } unset($column['type']); @@ -322,7 +342,7 @@ public static function seed(string $fileToSeed): bool } } - $parsedData[$key] = $localFakerInstance; + $parsedData[$key] = is_array($localFakerInstance) ? implode('-', $localFakerInstance) : $localFakerInstance; continue; }