From c4af2aac41133f914746d668df63e07baa606c32 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 15 Jan 2026 12:48:35 +0300
Subject: [PATCH 01/88] ci: Updated Workflows
- Added PHP 8.5
- Add Fixed Version for Re-Usable Workflows
- Renamed Workflows by removing the word 'Build'
---
.github/workflows/php81.yaml | 4 +-
.github/workflows/php82.yml | 4 +-
.github/workflows/php83.yml | 4 +-
.github/workflows/php84.yml | 16 +----
.github/workflows/php85.yml | 128 +++++++++++++++++++++++++++++++++++
5 files changed, 135 insertions(+), 21 deletions(-)
create mode 100644 .github/workflows/php85.yml
diff --git a/.github/workflows/php81.yaml b/.github/workflows/php81.yaml
index 0f9bfadd2..cdacb46ce 100644
--- a/.github/workflows/php81.yaml
+++ b/.github/workflows/php81.yaml
@@ -1,4 +1,4 @@
-name: Build PHP 8.1
+name: PHP 8.1
on:
push:
@@ -104,7 +104,7 @@ jobs:
code-coverage:
name: Coverage
needs: test
- uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
+ uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
with:
php-version: '8.1'
coverage-file: 'php-8.1-coverage.xml'
diff --git a/.github/workflows/php82.yml b/.github/workflows/php82.yml
index a162da5a0..68f379762 100644
--- a/.github/workflows/php82.yml
+++ b/.github/workflows/php82.yml
@@ -1,4 +1,4 @@
-name: Build PHP 8.2
+name: PHP 8.2
on:
push:
@@ -105,7 +105,7 @@ jobs:
code-coverage:
name: Coverage
needs: test
- uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
+ uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
with:
php-version: '8.2'
coverage-file: 'php-8.2-coverage.xml'
diff --git a/.github/workflows/php83.yml b/.github/workflows/php83.yml
index 805dd4da0..7afb7d0e5 100644
--- a/.github/workflows/php83.yml
+++ b/.github/workflows/php83.yml
@@ -1,4 +1,4 @@
-name: Build PHP 8.3
+name: PHP 8.3
on:
push:
@@ -105,7 +105,7 @@ jobs:
code-coverage:
name: Coverage
needs: test
- uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
+ uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
with:
php-version: '8.3'
coverage-file: 'php-8.3-coverage.xml'
diff --git a/.github/workflows/php84.yml b/.github/workflows/php84.yml
index 282ed4545..33700f172 100644
--- a/.github/workflows/php84.yml
+++ b/.github/workflows/php84.yml
@@ -106,23 +106,9 @@ jobs:
code-coverage:
name: Coverage
needs: test
- uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@main
+ uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
with:
php-version: '8.4'
coverage-file: 'php-8.4-coverage.xml'
secrets:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
-
- code-quality:
- name: Code Quality
- needs: test
- uses: WebFiori/workflows/.github/workflows/quality-sonarcloud.yaml@main
- secrets:
- SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
-
- release-prod:
- name: Prepare Production Release Branch / Publish Release
- needs: [code-coverage, code-quality]
- uses: WebFiori/workflows/.github/workflows/release-php.yaml@main
- with:
- branch: 'main'
\ No newline at end of file
diff --git a/.github/workflows/php85.yml b/.github/workflows/php85.yml
new file mode 100644
index 000000000..62cdbab71
--- /dev/null
+++ b/.github/workflows/php85.yml
@@ -0,0 +1,128 @@
+name: PHP 8.5
+
+on:
+ push:
+ branches: [ main, dev ]
+ pull_request:
+ branches: [ main ]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ env:
+ SA_SQL_SERVER_PASSWORD: ${{ secrets.SA_SQL_SERVER_PASSWORD }}
+ MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
+
+ services:
+ sqlserver:
+ image: mcr.microsoft.com/mssql/server:2019-latest
+ env:
+ SA_PASSWORD: ${{ secrets.SA_SQL_SERVER_PASSWORD }}
+ ACCEPT_EULA: Y
+ MSSQL_PID: Express
+ ports:
+ - "1433:1433"
+ mysql:
+ image: mysql:8.0
+ env:
+ MYSQL_ROOT_PASSWORD: ${{ secrets.MYSQL_ROOT_PASSWORD }}
+ MYSQL_DATABASE: testing_db
+ MYSQL_ROOT_HOST: '%'
+ ports:
+ - 3306:3306
+ options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+ strategy:
+ fail-fast: true
+
+ name: Run PHPUnit Tests
+
+ steps:
+ - name: Clone Repo
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.5
+ extensions: mysqli, mbstring, sqlsrv
+ tools: phpunit:11.5.27, composer
+
+ - name: Install ODBC Driver for SQL Server
+ run: |
+ curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc
+ curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
+ sudo apt update
+ sudo ACCEPT_EULA=Y apt install mssql-tools18 unixodbc-dev msodbcsql18
+
+ - name: Wait for SQL Server
+ run: |
+ for i in {1..12}; do
+ if /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P '${{ secrets.SA_SQL_SERVER_PASSWORD }}' -Q 'SELECT 1' -C > /dev/null 2>&1; then
+ echo "SQL Server is ready"
+ break
+ fi
+ echo "Waiting for SQL Server... ($i/12)"
+ sleep 10
+ done
+
+ - name: Create SQL Server Database
+ run: /opt/mssql-tools18/bin/sqlcmd -S localhost -U SA -P '${{ secrets.SA_SQL_SERVER_PASSWORD }}' -Q 'create database testing_db' -C
+
+ - name: Setup MySQL Client
+ run: |
+ sudo apt update
+ sudo apt install mysql-client-core-8.0
+
+ - name: Wait for MySQL
+ run: |
+ until mysqladmin ping -h 127.0.0.1 --silent; do
+ echo 'waiting for mysql...'
+ sleep 1
+ done
+
+ - name: Create MySQL Database
+ run: |
+ mysql -h 127.0.0.1 -u root -p${{ secrets.MYSQL_ROOT_PASSWORD }} -e "CREATE DATABASE IF NOT EXISTS testing_db;"
+
+ - name: Install Dependencies
+ run: composer install --prefer-source --no-interaction
+
+ - name: Execute Tests
+ run: phpunit --configuration=tests/phpunit10.xml --coverage-clover=clover.xml
+
+ - name: Rename coverage report
+ run: |
+ mv clover.xml php-8.5-coverage.xml
+
+ - name: Upload Coverage Report
+ uses: actions/upload-artifact@v4
+ with:
+ name: code-coverage
+ path: php-8.5-coverage.xml
+
+
+ code-coverage:
+ name: Coverage
+ needs: test
+ uses: WebFiori/workflows/.github/workflows/coverage-codecov.yaml@v1.2.1
+ with:
+ php-version: '8.5'
+ coverage-file: 'php-8.5-coverage.xml'
+ secrets:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
+
+ code-quality:
+ name: Code Quality
+ needs: test
+ uses: WebFiori/workflows/.github/workflows/quality-sonarcloud.yaml@v1.2.1
+ secrets:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+
+ release-prod:
+ name: Prepare Production Release Branch / Publish Release
+ needs: [code-coverage, code-quality]
+ uses: WebFiori/workflows/.github/workflows/release-php.yaml@v1.2.1
+ with:
+ branch: 'main'
\ No newline at end of file
From 9f9aff6591eef9cd4a4256979542eba44ce6e7bd Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 15 Jan 2026 13:07:36 +0300
Subject: [PATCH 02/88] ci: Update PHPUnit Version for PHP 8.5
---
.github/workflows/php85.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/php85.yml b/.github/workflows/php85.yml
index 62cdbab71..287d99f5a 100644
--- a/.github/workflows/php85.yml
+++ b/.github/workflows/php85.yml
@@ -47,7 +47,7 @@ jobs:
with:
php-version: 8.5
extensions: mysqli, mbstring, sqlsrv
- tools: phpunit:11.5.27, composer
+ tools: phpunit:12.5.4, composer
- name: Install ODBC Driver for SQL Server
run: |
From 778128fe12814ff3f21e313aef580b10331b335a Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 15 Jan 2026 13:15:10 +0300
Subject: [PATCH 03/88] ci: Added Concurrency Control
---
.github/workflows/php81.yaml | 5 +++++
.github/workflows/php82.yml | 4 ++++
.github/workflows/php83.yml | 4 ++++
.github/workflows/php84.yml | 4 ++++
.github/workflows/php85.yml | 4 ++++
5 files changed, 21 insertions(+)
diff --git a/.github/workflows/php81.yaml b/.github/workflows/php81.yaml
index cdacb46ce..83660001d 100644
--- a/.github/workflows/php81.yaml
+++ b/.github/workflows/php81.yaml
@@ -5,6 +5,11 @@ on:
branches: [ main ]
pull_request:
branches: [ main ]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-latest
diff --git a/.github/workflows/php82.yml b/.github/workflows/php82.yml
index 68f379762..df1fd71e0 100644
--- a/.github/workflows/php82.yml
+++ b/.github/workflows/php82.yml
@@ -6,6 +6,10 @@ on:
pull_request:
branches: [ main ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-latest
diff --git a/.github/workflows/php83.yml b/.github/workflows/php83.yml
index 7afb7d0e5..594ca1f56 100644
--- a/.github/workflows/php83.yml
+++ b/.github/workflows/php83.yml
@@ -6,6 +6,10 @@ on:
pull_request:
branches: [ main, dev ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-latest
diff --git a/.github/workflows/php84.yml b/.github/workflows/php84.yml
index 33700f172..b691330fc 100644
--- a/.github/workflows/php84.yml
+++ b/.github/workflows/php84.yml
@@ -6,6 +6,10 @@ on:
pull_request:
branches: [ main ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-latest
diff --git a/.github/workflows/php85.yml b/.github/workflows/php85.yml
index 287d99f5a..5f2ba4d24 100644
--- a/.github/workflows/php85.yml
+++ b/.github/workflows/php85.yml
@@ -6,6 +6,10 @@ on:
pull_request:
branches: [ main ]
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-latest
From 41c42caf2d2868b54513ea2b4e78662b78c7a230 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 15 Jan 2026 13:15:39 +0300
Subject: [PATCH 04/88] docs(readme): Updated PHP Version
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 74a9f8e6c..13c9c1353 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
@@ -34,7 +34,7 @@ WebFiori Framework is a mini web development framework which is built using PHP
|
|
|
|
|
|
-
+|
|
## Key Features
From 29a9e00c2a498ba912d916a74b00e9a18408fc87 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 15 Jan 2026 13:16:14 +0300
Subject: [PATCH 05/88] chore: Remove Funding Info
They already exist in .github
---
.github/FUNDING.yml | 3 ---
1 file changed, 3 deletions(-)
delete mode 100644 .github/FUNDING.yml
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
deleted file mode 100644
index 2213db043..000000000
--- a/.github/FUNDING.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-# These are supported funding model platforms
-custom: ["https://paypal.me/IbrahimBinAlshikh", "https://www.buymeacoffee.com/ibrahimdev"]
-ko_fi: ibrahimdev
From 868e123f2d8d75e12b2d9160d9425ddaa105ffc2 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 19 Jan 2026 22:45:03 +0300
Subject: [PATCH 06/88] fix: Request Method not Allowed
---
WebFiori/Framework/Router/Router.php | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/WebFiori/Framework/Router/Router.php b/WebFiori/Framework/Router/Router.php
index 47d0d8f0a..2b8389ed2 100644
--- a/WebFiori/Framework/Router/Router.php
+++ b/WebFiori/Framework/Router/Router.php
@@ -1378,7 +1378,8 @@ private function resolveUrlHelper(string $uri, bool $loadResource = true) {
* @throws RoutingException
*/
private function routeFound(RouterUri $route, bool $loadResource) {
- if ($route->isRequestMethodAllowed()) {
+
+ if ($route->isRequestMethodAllowed((App::getRequest()->getMethod()))) {
$this->uriObj = $route;
foreach ($route->getMiddleware() as $mw) {
From c67616e0e3bda1174251c0219b8efbd95fafaf08 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 19 Jan 2026 23:31:56 +0300
Subject: [PATCH 07/88] refactor: Migrations Creation Process
---
.../Framework/Cli/Helpers/CreateMigration.php | 98 +++++++--
.../Writers/DatabaseMigrationWriter.php | 154 -------------
.../Tests/Cli/CreateMigrationTest.php | 146 ++++++++++---
.../Writers/DatabaseMigrationWriterTest.php | 205 ------------------
4 files changed, 204 insertions(+), 399 deletions(-)
delete mode 100644 WebFiori/Framework/Writers/DatabaseMigrationWriter.php
delete mode 100644 tests/WebFiori/Framework/Tests/Writers/DatabaseMigrationWriterTest.php
diff --git a/WebFiori/Framework/Cli/Helpers/CreateMigration.php b/WebFiori/Framework/Cli/Helpers/CreateMigration.php
index 2dc0c16a6..6efe737fb 100644
--- a/WebFiori/Framework/Cli/Helpers/CreateMigration.php
+++ b/WebFiori/Framework/Cli/Helpers/CreateMigration.php
@@ -10,37 +10,109 @@
*/
namespace WebFiori\Framework\Cli\Helpers;
-use WebFiori\Database\Schema\SchemaRunner;
+use WebFiori\Database\Schema\DatabaseChangeGenerator;
+use WebFiori\Database\Schema\GeneratorOption;
use WebFiori\Framework\Cli\CLIUtils;
use WebFiori\Framework\Cli\Commands\CreateCommand;
-use WebFiori\Framework\Writers\DatabaseMigrationWriter;
/**
- * A helper class which is used to help in creating scheduler tasks classes using CLI.
+ * A helper class which is used to help in creating migration classes using CLI.
*
* @author Ibrahim
*
* @version 1.0
*/
-class CreateMigration extends CreateClassHelper {
- private $isConfigured;
+class CreateMigration {
+ private $command;
+ private $generator;
+ private $className;
+ private $dependencies = [];
+
/**
* Creates new instance of the class.
*
* @param CreateCommand $command A command that is used to call the class.
*/
public function __construct(CreateCommand $command) {
- $ns = APP_DIR.'\\Database\\migrations';
+ $this->command = $command;
+ $this->generator = new DatabaseChangeGenerator();
+
+ $ns = APP_DIR.'\\Database\\Migrations';
if (!$command->isArgProvided('--defaults')) {
$ns = CLIUtils::readNamespace($command, $ns , 'Migration namespace:');
}
-
- $runner = new SchemaRunner(new \WebFiori\Database\ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- parent::__construct($command, new DatabaseMigrationWriter($runner));
- $this->setNamespace($ns);
- $this->setClassName($command->readClassName('Provide a name for the class that will have migration logic:', null));
+ $this->generator->setNamespace($ns);
+ $this->generator->setPath(APP_PATH.'Database'.DS.'Migrations');
+
+ $this->className = $command->readClassName('Provide a name for the class that will have migration logic:', null);
+
+ if (!$command->isArgProvided('--defaults')) {
+ $this->readDependencies();
+ }
}
- public function isConfigured() : bool {
- return $this->isConfigured;
+
+ public function writeClass() {
+ $options = [];
+
+ if (!empty($this->dependencies)) {
+ $options[GeneratorOption::DEPENDENCIES] = $this->dependencies;
+ }
+
+ $filePath = $this->generator->createMigration($this->className, $options);
+ $this->command->info('New class was created at "'.dirname($filePath).'".');
+ }
+
+ private function readDependencies() {
+ if (!$this->command->confirm('Does this migration depend on other migrations?', false)) {
+ return;
+ }
+
+ $migrations = $this->getExistingMigrations();
+
+ if (empty($migrations)) {
+ $this->command->warning('No existing migrations found.');
+ return;
+ }
+
+ $this->command->println('Available migrations:');
+ foreach ($migrations as $idx => $migration) {
+ $this->command->println("$idx: $migration");
+ }
+
+ while (true) {
+ $input = $this->command->getInput('Enter migration number (or press Enter to finish):');
+
+ if (empty($input)) {
+ break;
+ }
+
+ $idx = (int)$input;
+ if (isset($migrations[$idx])) {
+ $fullClass = '\\'.$this->generator->getNamespace().'\\'.$migrations[$idx];
+ $this->dependencies[] = $fullClass;
+ $this->command->success("Added dependency: {$migrations[$idx]}");
+ } else {
+ $this->command->error('Invalid migration number.');
+ }
+ }
+ }
+
+ private function getExistingMigrations() : array {
+ $migrationsDir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (!is_dir($migrationsDir)) {
+ return [];
+ }
+
+ $files = scandir($migrationsDir);
+ $migrations = [];
+
+ foreach ($files as $file) {
+ if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
+ $migrations[] = pathinfo($file, PATHINFO_FILENAME);
+ }
+ }
+
+ return $migrations;
}
}
diff --git a/WebFiori/Framework/Writers/DatabaseMigrationWriter.php b/WebFiori/Framework/Writers/DatabaseMigrationWriter.php
deleted file mode 100644
index 355952ee8..000000000
--- a/WebFiori/Framework/Writers/DatabaseMigrationWriter.php
+++ /dev/null
@@ -1,154 +0,0 @@
-runner = $runner;
- $name = $this->generateMigrationName();
-
- $this->setClassName($name);
-
- parent::__construct($name, APP_PATH.'Database'.DS.'Migrations', APP_DIR.'\\Database\\Migrations');
- $this->addUseStatement([
- Database::class,
- AbstractMigration::class,
- ]);
-
- }
-
- private function generateMigrationName() {
- $name = 'Migration' . str_pad(self::$migrationCounter, 3, '0', STR_PAD_LEFT);
- self::$migrationCounter++;
- return $name;
- }
-
- /**
- * Add an environment where this migration should run.
- */
- public function addEnv(string $env) {
- $this->environments[] = $env;
- }
-
- /**
- * Add a dependency migration class name.
- */
- public function addDependency(string $dependency) : bool {
- if (class_exists($dependency)) {
- $this->dependencies[] = $dependency;
- $this->addUseStatement($dependency);
- return true;
- }
- return false;
- }
-
- /**
- * Reset the migration counter for testing purposes.
- */
- public static function resetCounter() {
- self::$migrationCounter = 0;
- }
-
- public function writeClassBody() {
- $this->append([
- '/**',
- ' * Creates new instance of the class.',
- ' */',
- $this->f('__construct'),
-
- ], 1);
- $this->append("parent::__construct();", 2);
- $this->append('}', 1);
-
- $this->append('/**', 1);
- $this->append(' * Get the list of migrations this migration depends on.', 1);
- $this->append(' * ', 1);
- $this->append(' * @return array Array of migration class names that must be executed before this one.', 1);
- $this->append(' */', 1);
- $this->append($this->f('getDependencies', [], 'array'), 1);
- if (empty($this->dependencies)) {
- $this->append('return [];', 2);
- } else {
- $this->append('return [', 2);
- foreach ($this->dependencies as $dep) {
- $this->append(" $dep::class,", 2);
- }
- $this->append('];', 2);
- }
- $this->append('}', 1);
-
- $this->append('/**', 1);
- $this->append(' * Get the environments where this migration should be executed.', 1);
- $this->append(' * ', 1);
- $this->append(' * @return array Empty array means all environments.', 1);
- $this->append(' */', 1);
- $this->append($this->f('getEnvironments', [], 'array'), 1);
- if (empty($this->environments)) {
- $this->append('return [];', 2);
- } else {
- $this->append('return [', 2);
- foreach ($this->environments as $env) {
- $this->append(" '$env',", 2);
- }
- $this->append('];', 2);
- }
- $this->append('}', 1);
-
- $this->append('/**', 1);
- $this->append(' * Performs the action that will apply the migration.', 1);
- $this->append(' * ', 1);
- $this->append(' * @param Database $db The database at which the migration will be applied to.', 1);
- $this->append(' */', 1);
- $this->append($this->f('up', ['db' => 'Database'], 'void'), 1);
- $this->append('//TODO: Implement the action which will apply the migration to database.', 2);
- $this->append('}', 1);
-
- $this->append('/**', 1);
- $this->append(' * Performs the action that will revert back the migration.', 1);
- $this->append(' * ', 1);
- $this->append(' * @param Database $db The database at which the migration will be applied to.', 1);
- $this->append(' */', 1);
- $this->append($this->f('down', ['db' => 'Database'], 'void'), 1);
- $this->append('//TODO: Implement the action which will revert back the migration.', 2);
- $this->append('}', 1);
- $this->append('}');
- }
- public function writeClassComment() {
- $classTop = [
- '/**',
- ' * A database migration class.',
- ' */'
- ];
- $this->append($classTop);
- }
-
- public function writeClassDeclaration() {
- $this->append('class '.$this->getName().' extends AbstractMigration {');
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php
index 7dff77f2d..1b853afc4 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php
@@ -1,7 +1,7 @@
assertEquals([
- "Migration namespace: Enter = 'App\Database\migrations'\n",
+ "Migration namespace: Enter = 'App\Database\Migrations'\n",
"Provide a name for the class that will have migration logic:\n",
+ "Does this migration depend on other migrations?(y/N)\n",
'Info: New class was created at "'. APP_PATH .'Database'.DS.'Migrations".'."\n",
], $this->executeMultiCommand([
CreateCommand::class,
@@ -57,26 +48,127 @@ public function testCreateMigration01() {
], [
"\n",
$name,
- "Great One",
- "11"
+ "n"
]));
$this->assertEquals(0, $this->getExitCode());
- // Check if file was written and require it
$filePath = APP_PATH . 'Database' . DS . 'Migrations' . DS . $name . '.php';
$this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
require_once $filePath;
$this->assertTrue(class_exists($clazz));
+
+ $instance = new $clazz();
+ $this->assertInstanceOf(AbstractMigration::class, $instance);
+ $this->assertEquals([], $instance->getDependencies());
+ $this->assertEquals([], $instance->getEnvironments());
+
$this->removeClass($clazz);
}
- private function getMName() {
- $runner = new SchemaRunner(null);
- $count = count($runner->getChanges());
- if ($count < 10) {
- return 'Migration00'.$count;
- } else if ($count < 100) {
- return 'Migration0'.$count;
- }
- return 'Migration'.$count;
+
+ /**
+ * @test
+ */
+ public function testCreateMigrationWithDependencies() {
+ // First create a base migration
+ $baseName = 'BaseMigration';
+
+ $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'migration',
+ ], [
+ "\n",
+ $baseName,
+ "n",
+ "n"
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $baseFile = APP_PATH . 'Database' . DS . 'Migrations' . DS . $baseName . '.php';
+ $this->assertTrue(file_exists($baseFile), "Base migration file should exist");
+
+ // Manually create a dependent migration using DatabaseChangeGenerator to verify it works
+ $generator = new \WebFiori\Database\Schema\DatabaseChangeGenerator();
+ $generator->setNamespace('App\\Database\\Migrations');
+ $generator->setPath(APP_PATH . 'Database' . DS . 'Migrations');
+ $generator->createMigration('ManualDependent', [
+ \WebFiori\Database\Schema\GeneratorOption::DEPENDENCIES => ['\\App\\Database\\Migrations\\BaseMigration']
+ ]);
+
+ $manualFile = APP_PATH . 'Database' . DS . 'Migrations' . DS . 'ManualDependent.php';
+ $this->assertTrue(file_exists($manualFile));
+ $content = file_get_contents($manualFile);
+ $this->assertStringContainsString('getDependencies', $content);
+ $this->assertStringContainsString('BaseMigration', $content);
+
+ require_once $baseFile;
+ require_once $manualFile;
+ $instance = new \App\Database\Migrations\ManualDependent();
+ $this->assertEquals(['App\\Database\\Migrations\\BaseMigration'], $instance->getDependencies());
+
+ $this->removeClass('\\App\\Database\\Migrations\\ManualDependent');
+ $this->removeClass('\\App\\Database\\Migrations\\'.$baseName);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateMigrationWithEnvironments() {
+ // Note: DatabaseChangeGenerator doesn't support environments for migrations yet
+ // This test is kept for future compatibility
+ $this->markTestSkipped('DatabaseChangeGenerator does not support environments for migrations yet');
+
+ $name = 'EnvMigration';
+ $clazz = '\\App\\Database\\Migrations\\'.$name;
+
+ $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'migration',
+ ], [
+ "\n",
+ $name,
+ "n",
+ "y",
+ "dev",
+ "y",
+ "staging",
+ "n"
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $filePath = APP_PATH . 'Database' . DS . 'Migrations' . DS . $name . '.php';
+ require_once $filePath;
+ $this->assertTrue(class_exists($clazz));
+
+ $instance = new $clazz();
+ $this->assertInstanceOf(AbstractMigration::class, $instance);
+ $this->assertEquals(['dev', 'staging'], $instance->getEnvironments());
+
+ $this->removeClass($clazz);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateMigrationWithDefaults() {
+ $name = 'DefaultMigration';
+ $clazz = '\\App\\Database\\Migrations\\'.$name;
+
+ $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'migration',
+ '--defaults' => ''
+ ], [
+ $name
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $filePath = APP_PATH . 'Database' . DS . 'Migrations' . DS . $name . '.php';
+ $this->assertTrue(file_exists($filePath));
+ require_once $filePath;
+ $this->assertTrue(class_exists($clazz));
+
+ $this->removeClass($clazz);
}
-}
\ No newline at end of file
+}
diff --git a/tests/WebFiori/Framework/Tests/Writers/DatabaseMigrationWriterTest.php b/tests/WebFiori/Framework/Tests/Writers/DatabaseMigrationWriterTest.php
deleted file mode 100644
index 9c3adaaf5..000000000
--- a/tests/WebFiori/Framework/Tests/Writers/DatabaseMigrationWriterTest.php
+++ /dev/null
@@ -1,205 +0,0 @@
-removeClass($clazz);
- $runner = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- $writter = new DatabaseMigrationWriter($runner);
- $this->assertEquals('Migration000', $writter->getName());
- $this->assertEquals('App\\Database\\Migrations', $writter->getNamespace());
- $this->assertEquals('', $writter->getSuffix());
- $this->assertEquals([
- "WebFiori\Database\Database",
- "WebFiori\Database\Schema\AbstractMigration",
- ], $writter->getUseStatements());
- $writter->writeClass();
-
- // Check if file was written and require it
- $filePath = $writter->getPath() . DS . $writter->getName() . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($clazz));
- $runner->register($clazz);
- $allClasses[] = $clazz;
- $migrations = $runner->getChanges();
- $this->assertEquals(1, count($migrations));
- $m00 = $migrations[0];
- $this->assertTrue($m00 instanceof AbstractMigration);
- $this->assertEquals('App\\Database\\Migrations\\Migration000', $m00->getName());
- $this->removeClass($clazz);
- $runner = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- }
- /**
- * @test
- */
- public function test01() {
- DatabaseMigrationWriter::resetCounter();
- $runner = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- $path = APP_PATH.DS.'Database'.DS.'Migrations';
- $ns = '\\App\\Database\\Migrations';
- $writter = new DatabaseMigrationWriter($runner);
- $writter->setClassName('MyMigration');
- $this->assertEquals('MyMigration', $writter->getName());
- $this->assertEquals('App\\Database\\Migrations', $writter->getNamespace());
-
- $writter->writeClass();
- $clazz = "\\App\\Database\\Migrations\\MyMigration";
-
- // Check if file was written and require it
- $filePath = $writter->getPath() . DS . $writter->getName() . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($clazz));
- $runner->register($clazz);
- $allClasses[] = $clazz;
- $migrations = $runner->getChanges();
- $this->assertEquals(1, count($migrations));
- $m00 = $migrations[0];
- $this->assertTrue($m00 instanceof AbstractMigration);
- $this->assertEquals('App\\Database\\Migrations\\MyMigration', $m00->getName());
- $this->removeClass($clazz);
- $runner = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- }
- /**
- * @test
- */
- public function test02() {
- DatabaseMigrationWriter::resetCounter();
- $runner = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- $path = APP_PATH.DS.'Database'.DS.'Migrations';
- $ns = '\\App\\Database\\Migrations';
- $writter = new DatabaseMigrationWriter($runner);
- $this->assertEquals('Migration000', $writter->getName());
- $writter->writeClass();
- $clazz = "\\App\\Database\\Migrations\\Migration000";
-
- // Check if file was written and require it
- $filePath = $writter->getPath() . DS . $writter->getName() . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($clazz));
- $runner->register($clazz);
- $allClasses[] = $clazz;
- $runner2 = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- $runner2->register($clazz);
- $migrations = $runner2->getChanges();
- $this->assertEquals(1, count($migrations));
- $m00 = $migrations[0];
- $this->assertTrue($m00 instanceof AbstractMigration);
- $this->assertEquals('App\\Database\\Migrations\\Migration000', $m00->getName());
-
- $writter2 = new DatabaseMigrationWriter($runner2);
- $this->assertEquals('Migration001', $writter2->getName());
- $writter2->writeClass();
- $clazz2 = "\\App\\Database\\Migrations\\Migration001";
-
- // Check if file was written and require it
- $filePath2 = $writter2->getPath() . DS . $writter2->getName() . '.php';
- $this->assertTrue(file_exists($filePath2), "Class file was not created: $filePath2");
- require_once $filePath2;
- $this->assertTrue(class_exists($clazz2));
- $runner->register($clazz);
- $allClasses[] = $clazz;
- $runner3 = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- $runner3->register($clazz);
- $runner3->register($clazz2);
- $migrations2 = $runner3->getChanges();
- $this->assertEquals(2, count($migrations2));
- $m01 = $migrations2[1];
- $this->assertTrue($m00 instanceof AbstractMigration);
- $this->assertEquals('App\\Database\\Migrations\\Migration001', $m01->getName());
- $this->removeClass($clazz);
- $runner = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- $this->removeClass($clazz2);
- }
- /**
- * @test
- */
- public function test03() {
- DatabaseMigrationWriter::resetCounter();
- $runner = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- $path = APP_PATH.DS.'Database'.DS.'Migrations';
- $ns = '\\App\\Database\\Migrations';
- $allClasses = [];
- for ($x = 0 ; $x < 110 ; $x++) {
- $writter = new DatabaseMigrationWriter($runner);
- if ($x < 10) {
- $name = 'Migration00'.$x;
- } else if ($x < 100) {
- $name = 'Migration0'.$x;
- } else {
- $name = 'Migration'.$x;
- }
- $this->assertEquals($name, $writter->getName());
- $writter->writeClass();
- $clazz = "\\App\\Database\\Migrations\\".$name;
-
- // Check if file was written and require it
- $filePath = $writter->getPath() . DS . $writter->getName() . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($clazz));
- $runner->register($clazz);
- $allClasses[] = $clazz;
- $xRunner = new SchemaRunner(new ConnectionInfo('mysql', 'test_user', 'test_pass', 'test_db'));
- foreach ($allClasses as $cls) {
- $xRunner->register($cls);
- }
-
- $migrations = $xRunner->getChanges();
- $this->assertEquals($x + 1, count($migrations));
- $m = $migrations[$x];
- $this->assertTrue($m instanceof AbstractMigration);
- $this->assertEquals("App\\Database\\Migrations\\" . $name, $m->getName());
- }
- foreach ($migrations as $m) {
- $this->removeClass("\\App\\Database\\Migrations\\".$m->getName());
- }
- }
- private function removeClass($classPath) {
- $file = new File(ROOT_PATH.$classPath.'.php');
- $file->remove();
- }
-}
From 237dd96cbda39cb25cbe1d272a3e990f7bb3fb6b Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 00:01:56 +0300
Subject: [PATCH 08/88] refactor: Migrations System Refactor
---
.../Cli/Commands/RunMigrationsCommand.php | 283 ++++++------------
.../Tests/Cli/RunMigrationsCommandTest.php | 171 +++--------
2 files changed, 142 insertions(+), 312 deletions(-)
diff --git a/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php
index fb65f9de2..017bcb869 100644
--- a/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php
+++ b/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php
@@ -14,8 +14,6 @@
use WebFiori\Cli\Argument;
use WebFiori\Cli\Command;
use WebFiori\Database\ConnectionInfo;
-use WebFiori\Database\Database;
-use WebFiori\Database\migration\AbstractMigration;
use WebFiori\Database\Schema\SchemaRunner;
use WebFiori\Framework\App;
use WebFiori\Framework\Cli\CLIUtils;
@@ -27,123 +25,67 @@
*/
class RunMigrationsCommand extends Command {
- private const EXIT_SUCCESS = 0;
- private const EXIT_ERROR = 1;
-
private ?SchemaRunner $runner = null;
- private ?ConnectionInfo $connection = null;
public function __construct() {
parent::__construct('migrations', [
- new Argument('--connection', 'The name of database connection to be used in executing the migrations.', true),
- new Argument('--runner', 'A class that extends the class "WebFiori\Database\Schema\SchemaRunner".', true),
- new Argument('--init', 'Creates migrations table in database if not exist.', true),
+ new Argument('--connection', 'The name of database connection to use.', true),
+ new Argument('--env', 'Environment name (dev, staging, production). Default: dev', true),
+ new Argument('--init', 'Create migrations tracking table.', true),
new Argument('--rollback', 'Rollback migrations.', true),
- new Argument('--all', 'If provided with --rollback, all migrations will be rolled back.', true),
+ new Argument('--batch', 'Rollback specific batch number.', true),
+ new Argument('--all', 'Rollback all migrations.', true),
+ new Argument('--dry-run', 'Preview changes without executing.', true),
], 'Execute database migrations.');
}
- /**
- * Execute the command.
- */
public function exec(): int {
try {
- if (!$this->initializeCommand()) {
- return self::EXIT_ERROR;
+ $connection = $this->getConnection();
+ if ($connection === null) {
+ return 1;
+ }
+
+ $env = $this->getArgValue('--env') ?? 'dev';
+ $this->runner = new SchemaRunner($connection, $env);
+
+ // Discover migrations
+ $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
+ $namespace = APP_DIR.'\\Database\\Migrations';
+ $count = $this->runner->discoverFromPath($migrationsPath, $namespace);
+
+ if ($count === 0 && !$this->isArgProvided('--init')) {
+ $this->info('No migrations found.');
+ return 0;
}
if ($this->isArgProvided('--init')) {
- return $this->initializeMigrationsTable();
+ return $this->initTable();
}
if ($this->isArgProvided('--rollback')) {
- return $this->executeRollback();
+ return $this->rollback();
+ }
+
+ if ($this->isArgProvided('--dry-run')) {
+ return $this->dryRun();
}
- return $this->executeMigrations();
+ return $this->runMigrations();
} catch (Throwable $e) {
$this->error('An exception was thrown.');
- $this->println('Exception Message: ' . $e->getMessage());
- $this->println('Code: ' . $e->getCode());
- $this->println('At: ' . $e->getFile());
- $this->println('Line: ' . $e->getLine());
- $this->println('Stack Trace: ');
- $this->println($e->getTraceAsString());
- return self::EXIT_ERROR;
- }
- }
-
- /**
- * Initialize command dependencies.
- */
- private function initializeCommand(): bool {
- $this->connection = $this->resolveConnection();
- if ($this->connection === null) {
- return false;
- }
-
- $this->runner = $this->createRunner();
- if ($this->runner === null) {
- $runnerClass = $this->getArgValue('--runner');
- if ($runnerClass !== null) {
- // Runner creation failed, return false
- return false;
- }
- // Create default runner with connection
- $this->runner = new SchemaRunner($this->connection);
- }
-
- // Set connection on runner if it doesn't have one
- if ($this->runner->getConnectionInfo() === null) {
- $this->runner = new SchemaRunner($this->connection);
- }
-
- return true;
- }
-
- /**
- * Create SchemaRunner instance from --runner argument.
- */
- private function createRunner(): ?SchemaRunner {
- $runnerClass = $this->getArgValue('--runner');
-
- if ($runnerClass === null) {
- return null; // Will be created later with connection
- }
-
- if (!class_exists($runnerClass)) {
- $this->error("The argument --runner has invalid value: Class \"$runnerClass\" does not exist.");
- return null;
- }
-
- try {
- $runner = new $runnerClass();
- } catch (Throwable $e) {
- $this->error("The argument --runner has invalid value: Exception: \"{$e->getMessage()}\".");
- return null;
+ $this->println('Message: ' . $e->getMessage());
+ $this->println('File: ' . $e->getFile() . ':' . $e->getLine());
+ return 1;
}
-
- if (!($runner instanceof SchemaRunner)) {
- $this->error("The argument --runner has invalid value: \"$runnerClass\" is not an instance of \"SchemaRunner\".");
- return null;
- }
-
- return $runner;
}
- /**
- * Resolve database connection.
- */
- private function resolveConnection(): ?ConnectionInfo {
- // Check if runner already has a connection
- if ($this->runner !== null && $this->runner->getConnectionInfo() !== null) {
- return $this->runner->getConnectionInfo();
- }
-
+ private function getConnection(): ?ConnectionInfo {
$connections = App::getConfig()->getDBConnections();
+
if (empty($connections)) {
- $this->info('No connections were found in application configuration.');
+ $this->info('No database connections configured.');
return null;
}
@@ -152,7 +94,7 @@ private function resolveConnection(): ?ConnectionInfo {
if ($connectionName !== null) {
$connection = App::getConfig()->getDBConnection($connectionName);
if ($connection === null) {
- $this->error("No connection was found which has the name '$connectionName'.");
+ $this->error("Connection '$connectionName' not found.");
return null;
}
return $connection;
@@ -161,132 +103,97 @@ private function resolveConnection(): ?ConnectionInfo {
return CLIUtils::getConnectionName($this);
}
- /**
- * Initialize migrations table.
- */
- private function initializeMigrationsTable(): int {
+ private function initTable(): int {
try {
- $this->println("Initializing migrations table...");
+ $this->println('Creating migrations tracking table...');
$this->runner->createSchemaTable();
- $this->success("Migrations table successfully created.");
- return self::EXIT_SUCCESS;
+ $this->success('Migrations table created successfully.');
+ return 0;
} catch (Throwable $e) {
- $this->error('Unable to create migrations table due to following:');
- $this->println($e->getMessage());
- return self::EXIT_ERROR;
+ $this->error('Failed to create migrations table: ' . $e->getMessage());
+ return 1;
}
}
- /**
- * Execute migrations rollback.
- */
- private function executeRollback(): int {
- $migrations = $this->runner->getChanges();
- if (empty($migrations)) {
- $this->info("No migrations found.");
- return self::EXIT_SUCCESS;
- }
-
- $this->println("Rolling back migrations...");
-
+ private function rollback(): int {
try {
if ($this->isArgProvided('--all')) {
- $rolledBack = $this->runner->rollbackUpTo(null);
+ $this->println('Rolling back all migrations...');
+ $rolled = $this->runner->rollbackUpTo(null);
+ } else if ($this->isArgProvided('--batch')) {
+ $batch = (int)$this->getArgValue('--batch');
+ $this->println("Rolling back batch $batch...");
+ $rolled = $this->runner->rollbackBatch($batch);
} else {
- $rolledBack = $this->rollbackLast();
+ $this->println('Rolling back last batch...');
+ $rolled = $this->runner->rollbackLastBatch();
}
- if (empty($rolledBack)) {
- $this->info("No migrations were rolled back.");
+ if (empty($rolled)) {
+ $this->info('No migrations to rollback.');
} else {
- foreach ($rolledBack as $migration) {
- $this->success("Migration '{$migration->getName()}' was successfully rolled back.");
+ foreach ($rolled as $change) {
+ $this->success('Rolled back: ' . $change->getName());
}
+ $this->info('Total rolled back: ' . count($rolled));
}
- return self::EXIT_SUCCESS;
-
+ return 0;
} catch (Throwable $e) {
- $this->error('Failed to execute migration due to following:');
- $this->println($e->getMessage() . ' (Line ' . $e->getLine() . ')');
- $this->warning('Execution stopped.');
- return self::EXIT_ERROR;
+ $this->error('Rollback failed: ' . $e->getMessage());
+ return 1;
}
}
- /**
- * Rollback the last applied migration.
- */
- private function rollbackLast(): array {
- $changes = $this->runner->getChanges();
- $lastApplied = null;
+ private function dryRun(): int {
+ $pending = $this->runner->getPendingChanges(true);
- // Find the last applied migration
- foreach ($changes as $change) {
- if ($this->runner->isApplied($change->getName())) {
- $lastApplied = $change;
- }
+ if (empty($pending)) {
+ $this->info('No pending migrations.');
+ return 0;
}
- if ($lastApplied === null) {
- return [];
+ $this->println('Pending migrations:');
+ foreach ($pending as $item) {
+ $this->println(' - ' . $item['change']->getName());
+ if (!empty($item['queries'])) {
+ $this->println(' Queries:');
+ foreach ($item['queries'] as $query) {
+ $this->println(' ' . $query);
+ }
+ }
}
- return $this->runner->rollbackUpTo($lastApplied->getName());
+ return 0;
}
- /**
- * Execute migrations.
- */
- private function executeMigrations(): int {
- $migrations = $this->runner->getChanges();
- if (empty($migrations)) {
- $this->info("No migrations found.");
- return self::EXIT_SUCCESS;
- }
+ private function runMigrations(): int {
+ $this->println('Running migrations...');
- $this->println("Starting to execute migrations...");
- $appliedMigrations = [];
+ $result = $this->runner->apply();
- try {
- while (($migration = $this->getNextMigration()) !== null) {
- $applied = $this->runner->applyOne();
- if ($applied !== null) {
- $this->success("Migration '{$applied->getName()}' applied successfully.");
- $appliedMigrations[] = $applied;
- } else {
- break;
- }
+ if ($result->hasApplied()) {
+ foreach ($result->getApplied() as $change) {
+ $this->success('Applied: ' . $change->getName());
}
-
- if (empty($appliedMigrations)) {
- $this->info("No migrations were executed.");
- } else {
- $this->info("Number of applied migrations: " . count($appliedMigrations));
- $this->println("Names of applied migrations:");
- $names = array_map(fn($m) => $m->getName(), $appliedMigrations);
- $this->printList($names);
+ }
+
+ if ($result->hasSkipped()) {
+ foreach ($result->getSkipped() as $item) {
+ $this->warning('Skipped: ' . $item['change']->getName() . ' (' . $item['reason'] . ')');
}
-
- return self::EXIT_SUCCESS;
-
- } catch (Throwable $e) {
- $this->error('Failed to execute migration due to following:');
- $this->println($e->getMessage() . ' (Line ' . $e->getLine() . ')');
- $this->warning('Execution stopped.');
- return self::EXIT_ERROR;
}
- }
-
- /**
- * Get the next migration to apply.
- */
- private function getNextMigration(): ?AbstractMigration {
- foreach ($this->runner->getChanges() as $migration) {
- if (!$this->runner->isApplied($migration->getName())) {
- return $migration;
+
+ if ($result->hasFailed()) {
+ foreach ($result->getFailed() as $item) {
+ $this->error('Failed: ' . $item['change']->getName());
+ $this->println(' Error: ' . $item['error']->getMessage());
}
}
- return null;
+
+ $this->info('Applied: ' . $result->count() . ' migrations');
+ $this->info('Time: ' . round($result->getTotalTime(), 2) . 'ms');
+
+ return $result->hasFailed() ? 1 : 0;
}
}
diff --git a/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php
index ba8bd931d..7c6d38698 100644
--- a/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php
@@ -2,7 +2,6 @@
namespace WebFiori\Framework\Test\Cli;
use WebFiori\Database\ConnectionInfo;
-use WebFiori\Database\Schema\SchemaRunner;
use WebFiori\Framework\App;
use WebFiori\Framework\Cli\CLITestCase;
use WebFiori\Framework\Cli\Commands\RunMigrationsCommand;
@@ -19,9 +18,11 @@ class RunMigrationsCommandTest extends CLITestCase {
protected function setUp(): void {
parent::setUp();
$this->setupTestConnection();
+ $this->cleanupMigrations();
}
protected function tearDown(): void {
+ $this->cleanupMigrations();
App::getConfig()->removeAllDBConnections();
parent::tearDown();
}
@@ -32,6 +33,17 @@ private function setupTestConnection(): void {
App::getConfig()->addOrUpdateDBConnection($this->testConnection);
}
+ private function cleanupMigrations(): void {
+ $dir = APP_PATH . 'Database' . DS . 'Migrations';
+ if (is_dir($dir)) {
+ foreach (glob($dir . DS . '*.php') as $file) {
+ if (basename($file) !== '.gitkeep') {
+ unlink($file);
+ }
+ }
+ }
+ }
+
/**
* @test
*/
@@ -42,7 +54,7 @@ public function testExecWithNoConnections(): void {
RunMigrationsCommand::class
]);
- $this->assertContains("Info: No connections were found in application configuration.\n", $output);
+ $this->assertContains("Info: No database connections configured.\n", $output);
$this->assertEquals(1, $this->getExitCode());
}
@@ -55,35 +67,7 @@ public function testExecWithInvalidConnection(): void {
'--connection' => 'invalid-connection'
]);
- $this->assertContains("Error: No connection was found which has the name 'invalid-connection'.\n", $output);
- $this->assertEquals(1, $this->getExitCode());
- }
-
- /**
- * @test
- */
- public function testExecWithInvalidRunnerClass(): void {
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection',
- '--runner' => 'NonExistentClass'
- ]);
-
- $this->assertContains("Error: The argument --runner has invalid value: Class \"NonExistentClass\" does not exist.\n", $output);
- $this->assertEquals(1, $this->getExitCode());
- }
-
- /**
- * @test
- */
- public function testExecWithInvalidRunnerType(): void {
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection',
- '--runner' => 'stdClass'
- ]);
-
- $this->assertContains("Error: The argument --runner has invalid value: \"stdClass\" is not an instance of \"SchemaRunner\".\n", $output);
+ $this->assertContains("Error: Connection 'invalid-connection' not found.\n", $output);
$this->assertEquals(1, $this->getExitCode());
}
@@ -97,20 +81,15 @@ public function testInitializeMigrationsTable(): void {
'--init'
]);
- $this->assertContains("Initializing migrations table...\n", $output);
+ $this->assertContains("Creating migrations tracking table...\n", $output);
+ $this->assertContains("Success: Migrations table created successfully.\n", $output);
$this->assertEquals(0, $this->getExitCode());
-
- // Verify table was actually created using mysqli
- $mysqli = new \mysqli('127.0.0.1', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', 3306);
- $result = $mysqli->query("SHOW TABLES LIKE 'schema_changes'");
- $this->assertEquals(1, $result->num_rows, 'Migrations table should be created');
- $mysqli->close();
}
/**
* @test
*/
- public function testExecuteMigrationsWithNoMigrations(): void {
+ public function testRunWithNoMigrations(): void {
$output = $this->executeMultiCommand([
RunMigrationsCommand::class,
'--connection' => 'test-connection'
@@ -123,103 +102,47 @@ public function testExecuteMigrationsWithNoMigrations(): void {
/**
* @test
*/
- public function testRollbackWithNoMigrations(): void {
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection',
- '--rollback'
- ]);
+ public function testDryRun(): void {
+ // Create a test migration
+ $this->createTestMigration('TestMigration');
- // Debug: Print actual output
- $this->assertEquals(0, $this->getExitCode());
- }
-
- /**
- * @test
- */
- public function testRollbackAllWithNoMigrations(): void {
$output = $this->executeMultiCommand([
RunMigrationsCommand::class,
'--connection' => 'test-connection',
- '--rollback',
- '--all'
+ '--dry-run'
]);
- // Debug: Print actual output
- $this->assertEquals(0, $this->getExitCode());
- }
-
- /**
- * @test
- */
- public function testExecuteMigrationsWithValidRunner(): void {
- $this->createTestMigrationRunner();
-
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection',
- '--runner' => 'TestMigrationRunner'
- ]);
- // The test runner has no migrations, so it should report no migrations found
- $this->assertContains("Info: No migrations found.\n", $output);
+ // Check if output contains expected text
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Pending migrations:', $outputStr);
+ $this->assertStringContainsString('TestMigration', $outputStr);
$this->assertEquals(0, $this->getExitCode());
-
- $this->cleanupTestMigrationRunner();
}
- /**
- * @test
- */
- public function testExceptionHandling(): void {
- $this->createFaultyMigrationRunner();
-
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection',
- '--runner' => 'FaultyMigrationRunner'
- ]);
-
- // The exception is caught during runner creation
- $this->assertContains("Error: The argument --runner has invalid value: Exception: \"Test exception\".\n", $output);
- $this->assertEquals(1, $this->getExitCode());
-
- $this->cleanupFaultyMigrationRunner();
- }
-
- private function createTestMigrationRunner(): void {
- $code = 'getDBConnection("test-connection");
- parent::__construct($conn);
- }
-}';
- file_put_contents(APP_PATH . 'TestMigrationRunner.php', $code);
- require_once APP_PATH . 'TestMigrationRunner.php';
- }
-
- private function cleanupTestMigrationRunner(): void {
- if (file_exists(APP_PATH . 'TestMigrationRunner.php')) {
- unlink(APP_PATH . 'TestMigrationRunner.php');
+ private function createTestMigration(string $name): void {
+ $dir = APP_PATH . 'Database' . DS . 'Migrations';
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
}
+
+ $content = <<getDBConnection("test-connection");
- parent::__construct($conn);
- throw new \Exception("Test exception");
- }
-}';
- file_put_contents(APP_PATH . 'FaultyMigrationRunner.php', $code);
- require_once APP_PATH . 'FaultyMigrationRunner.php';
+ public function down(Database \$db): void {
+ // Test rollback
}
-
- private function cleanupFaultyMigrationRunner(): void {
- if (file_exists(APP_PATH . 'FaultyMigrationRunner.php')) {
- unlink(APP_PATH . 'FaultyMigrationRunner.php');
- }
+}
+PHP;
+
+ file_put_contents($dir . DS . $name . '.php', $content);
}
}
From 3b1c77e2555ea8943d5c4f137afeadbb50eab405 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 17:41:08 +0300
Subject: [PATCH 09/88] Create AttributeTableWriter.php
---
.../Writers/AttributeTableWriter.php | 81 +++++++++++++++++++
1 file changed, 81 insertions(+)
create mode 100644 WebFiori/Framework/Writers/AttributeTableWriter.php
diff --git a/WebFiori/Framework/Writers/AttributeTableWriter.php b/WebFiori/Framework/Writers/AttributeTableWriter.php
new file mode 100644
index 000000000..129518884
--- /dev/null
+++ b/WebFiori/Framework/Writers/AttributeTableWriter.php
@@ -0,0 +1,81 @@
+addUseStatement([
+ 'WebFiori\\Database\\Attributes\\Column',
+ 'WebFiori\\Database\\Attributes\\Table',
+ 'WebFiori\\Database\\DataType'
+ ]);
+ }
+
+ public function setTableName(string $name) {
+ $this->tableName = $name;
+ }
+
+ public function addColumn(string $name, string $type, array $options = []) {
+ $this->columns[] = array_merge([
+ 'name' => $name,
+ 'type' => $type
+ ], $options);
+ }
+
+ public function writeClassBody() {
+ $this->append('}');
+ }
+
+ public function writeClassComment() {
+ $this->append('/**');
+ $this->append(' * Table definition using PHP 8 attributes.');
+ $this->append(' */');
+
+ // Add Table attribute
+ $this->append("#[Table(name: '{$this->tableName}')]", 0);
+
+ // Add Column attributes
+ foreach ($this->columns as $col) {
+ $attr = "#[Column(name: '{$col['name']}', type: DataType::{$col['type']}";
+
+ if (isset($col['size'])) {
+ $attr .= ", size: {$col['size']}";
+ }
+ if (isset($col['primary']) && $col['primary']) {
+ $attr .= ", primary: true";
+ }
+ if (isset($col['autoIncrement']) && $col['autoIncrement']) {
+ $attr .= ", autoIncrement: true";
+ }
+ if (isset($col['nullable']) && $col['nullable']) {
+ $attr .= ", nullable: true";
+ }
+
+ $attr .= ')]';
+ $this->append($attr, 0);
+ }
+ }
+
+ public function writeClassDeclaration() {
+ $this->append('class '.$this->getName().' {');
+ }
+}
From 92b86f6aa5a014d5f19277ad2ebb5f5e7278375e Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 17:41:31 +0300
Subject: [PATCH 10/88] Create DomainEntityWriter.php
---
.../Framework/Writers/DomainEntityWriter.php | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 WebFiori/Framework/Writers/DomainEntityWriter.php
diff --git a/WebFiori/Framework/Writers/DomainEntityWriter.php b/WebFiori/Framework/Writers/DomainEntityWriter.php
new file mode 100644
index 000000000..cc0b7f2b4
--- /dev/null
+++ b/WebFiori/Framework/Writers/DomainEntityWriter.php
@@ -0,0 +1,62 @@
+properties[] = [
+ 'name' => $name,
+ 'type' => $type,
+ 'nullable' => $nullable
+ ];
+ }
+
+ public function writeClassBody() {
+ $this->writeConstructor();
+ $this->append('}');
+ }
+
+ public function writeClassComment() {
+ $this->append([
+ '/**',
+ ' * Domain entity - pure PHP, no framework dependencies.',
+ ' */'
+ ]);
+ }
+
+ public function writeClassDeclaration() {
+ $this->append('class '.$this->getName().' {');
+ }
+
+ private function writeConstructor() {
+ $this->append('public function __construct(', 1);
+
+ $params = [];
+ foreach ($this->properties as $prop) {
+ $type = $prop['nullable'] ? '?'.$prop['type'] : $prop['type'];
+ $params[] = " public $type \${$prop['name']}";
+ }
+
+ $this->append(implode(",\n", $params));
+ $this->append(' ) {}', 0);
+ }
+}
From 3754c0349fc81f2778cabdbd0fb5e3e60b534c5d Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 17:41:53 +0300
Subject: [PATCH 11/88] Create RepositoryWriter.php
---
.../Framework/Writers/RepositoryWriter.php | 110 ++++++++++++++++++
1 file changed, 110 insertions(+)
create mode 100644 WebFiori/Framework/Writers/RepositoryWriter.php
diff --git a/WebFiori/Framework/Writers/RepositoryWriter.php b/WebFiori/Framework/Writers/RepositoryWriter.php
new file mode 100644
index 000000000..fc33c12a5
--- /dev/null
+++ b/WebFiori/Framework/Writers/RepositoryWriter.php
@@ -0,0 +1,110 @@
+addUseStatement([
+ 'WebFiori\\Database\\Repository\\AbstractRepository'
+ ]);
+ }
+
+ public function setEntityClass(string $class) {
+ $this->entityClass = $class;
+ $this->addUseStatement($class);
+ }
+
+ public function setTableName(string $name) {
+ $this->tableName = $name;
+ }
+
+ public function setIdField(string $field) {
+ $this->idField = $field;
+ }
+
+ public function addProperty(string $name, string $type) {
+ $this->properties[] = ['name' => $name, 'type' => $type];
+ }
+
+ public function writeClassBody() {
+ $this->writeGetTableName();
+ $this->writeGetIdField();
+ $this->writeToEntity();
+ $this->writeToArray();
+ $this->append('}');
+ }
+
+ public function writeClassComment() {
+ $this->append([
+ '/**',
+ ' * Repository for '.$this->entityClass.' entities.',
+ ' */'
+ ]);
+ }
+
+ public function writeClassDeclaration() {
+ $this->append('class '.$this->getName().' extends AbstractRepository {');
+ }
+
+ private function writeGetTableName() {
+ $this->append($this->f('getTableName', [], 'string').' {', 1);
+ $this->append("return '{$this->tableName}';", 2);
+ $this->append('}', 1);
+ $this->append('', 1);
+ }
+
+ private function writeGetIdField() {
+ $this->append($this->f('getIdField', [], 'string').' {', 1);
+ $this->append("return '{$this->idField}';", 2);
+ $this->append('}', 1);
+ $this->append('', 1);
+ }
+
+ private function writeToEntity() {
+ $entityShortName = basename(str_replace('\\', '/', $this->entityClass));
+ $this->append($this->f('toEntity', ['row' => 'array'], $entityShortName).' {', 1);
+ $this->append("return new $entityShortName(", 2);
+
+ $params = [];
+ foreach ($this->properties as $prop) {
+ $cast = $prop['type'] === 'int' ? '(int) ' : '';
+ $params[] = " {$cast}\$row['{$prop['name']}']";
+ }
+
+ $this->append(implode(",\n", $params));
+ $this->append(' );', 0);
+ $this->append('}', 1);
+ $this->append('', 1);
+ }
+
+ private function writeToArray() {
+ $this->append($this->f('toArray', ['entity' => 'object'], 'array').' {', 1);
+ $this->append('return [', 2);
+
+ foreach ($this->properties as $prop) {
+ $this->append("'{$prop['name']}' => \$entity->{$prop['name']},", 3);
+ }
+
+ $this->append('];', 2);
+ $this->append('}', 1);
+ }
+}
From 5067f41da62f035b362bc9ec612844ba7a83d467 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 17:42:02 +0300
Subject: [PATCH 12/88] Create RestServiceWriter.php
---
.../Framework/Writers/RestServiceWriter.php | 115 ++++++++++++++++++
1 file changed, 115 insertions(+)
create mode 100644 WebFiori/Framework/Writers/RestServiceWriter.php
diff --git a/WebFiori/Framework/Writers/RestServiceWriter.php b/WebFiori/Framework/Writers/RestServiceWriter.php
new file mode 100644
index 000000000..0ff9b862e
--- /dev/null
+++ b/WebFiori/Framework/Writers/RestServiceWriter.php
@@ -0,0 +1,115 @@
+setSuffix('Service');
+ $this->addUseStatement([
+ 'WebFiori\\Http\\WebService',
+ 'WebFiori\\Http\\Annotations\\RestController',
+ 'WebFiori\\Http\\Annotations\\GetMapping',
+ 'WebFiori\\Http\\Annotations\\PostMapping',
+ 'WebFiori\\Http\\Annotations\\PutMapping',
+ 'WebFiori\\Http\\Annotations\\DeleteMapping',
+ 'WebFiori\\Http\\Annotations\\Param',
+ 'WebFiori\\Http\\Annotations\\ResponseBody',
+ 'WebFiori\\Http\\Annotations\\AllowAnonymous',
+ 'WebFiori\\Http\\ParamType'
+ ]);
+ }
+
+ public function setDescription(string $desc) {
+ $this->description = $desc;
+ }
+
+ public function addMethod(string $httpMethod, string $methodName, array $params = [], string $returnType = 'array') {
+ $this->methods[] = [
+ 'http' => $httpMethod,
+ 'name' => $methodName,
+ 'params' => $params,
+ 'return' => $returnType
+ ];
+ }
+
+ public function writeClassBody() {
+ foreach ($this->methods as $method) {
+ $this->writeMethod($method);
+ }
+ $this->append('}');
+ }
+
+ public function writeClassComment() {
+ $serviceName = strtolower(str_replace('Service', '', $this->getName()));
+ $this->append('/**');
+ $this->append(' * '.$this->description);
+ $this->append(' */');
+ $this->append("#[RestController('$serviceName', '{$this->description}')]", 0);
+ }
+
+ public function writeClassDeclaration() {
+ $this->append('class '.$this->getName().' extends WebService {');
+ }
+
+ private function writeMethod(array $method) {
+ $this->append('', 1);
+ $mapping = ucfirst(strtolower($method['http'])).'Mapping';
+ $this->append("#[$mapping]", 1);
+ $this->append('#[ResponseBody]', 1);
+ $this->append('#[AllowAnonymous]', 1);
+
+ foreach ($method['params'] as $param) {
+ $paramAttr = "#[Param('{$param['name']}', ParamType::{$param['type']}, '{$param['description']}'";
+ if (isset($param['min'])) {
+ $paramAttr .= ", min: {$param['min']}";
+ }
+ if (isset($param['max'])) {
+ $paramAttr .= ", max: {$param['max']}";
+ }
+ $paramAttr .= ')]';
+ $this->append($paramAttr, 1);
+ }
+
+ $signature = 'public function '.$method['name'].'(';
+ $paramList = [];
+ foreach ($method['params'] as $param) {
+ $type = $this->mapParamType($param['type']);
+ $paramList[] = "?$type \${$param['name']} = null";
+ }
+ $signature .= implode(', ', $paramList);
+ $signature .= '): '.$method['return'].' {';
+
+ $this->append($signature, 1);
+ $this->append('// TODO: Implement method logic', 2);
+ $this->append('return [];', 2);
+ $this->append('}', 1);
+ }
+
+ private function mapParamType(string $type): string {
+ return match($type) {
+ 'INT' => 'int',
+ 'STRING', 'EMAIL', 'URL' => 'string',
+ 'DOUBLE' => 'float',
+ 'BOOL' => 'bool',
+ 'ARRAY' => 'array',
+ default => 'string'
+ };
+ }
+}
From 8faefd774b1629e996256758fc3270d7856d0bb1 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 17:42:13 +0300
Subject: [PATCH 13/88] Create CreateDomainEntity.php
---
.../Cli/Helpers/CreateDomainEntity.php | 54 +++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Helpers/CreateDomainEntity.php
diff --git a/WebFiori/Framework/Cli/Helpers/CreateDomainEntity.php b/WebFiori/Framework/Cli/Helpers/CreateDomainEntity.php
new file mode 100644
index 000000000..cb351138c
--- /dev/null
+++ b/WebFiori/Framework/Cli/Helpers/CreateDomainEntity.php
@@ -0,0 +1,54 @@
+isArgProvided('--defaults')) {
+ $ns = $this->getCommand()->getInput('Entity namespace: Enter = \''.$ns.'\'') ?: $ns;
+ }
+
+ $this->setNamespace($ns);
+ $this->setClassName($command->readClassName('Enter entity class name:'));
+
+ if (!$command->isArgProvided('--defaults')) {
+ $this->readProperties();
+ }
+ }
+
+ private function readProperties() {
+ $this->println('Add properties to the entity:');
+
+ while (true) {
+ $name = $this->getInput('Property name (or press Enter to finish):');
+ if (empty($name)) {
+ break;
+ }
+
+ $type = $this->select('Property type:', ['int', 'string', 'bool', 'float', 'array'], 1);
+ $nullable = $this->confirm('Is nullable?', false);
+
+ $this->getWriter()->addProperty($name, $type, $nullable);
+ $this->success("Added property: $name");
+ }
+ }
+}
From 6f4a7fc315667081ca22893068a4f744003b7344 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 17:59:18 +0300
Subject: [PATCH 14/88] feat: Create Attributes Table
---
.../Framework/Cli/Commands/CreateCommand.php | 10 ++
.../Cli/Helpers/CreateAttributeTable.php | 77 +++++++++++++++
.../Tests/Cli/CreateAttributeTableTest.php | 98 +++++++++++++++++++
3 files changed, 185 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Helpers/CreateAttributeTable.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateAttributeTableTest.php
diff --git a/WebFiori/Framework/Cli/Commands/CreateCommand.php b/WebFiori/Framework/Cli/Commands/CreateCommand.php
index bc5c2d3d6..744000a5a 100644
--- a/WebFiori/Framework/Cli/Commands/CreateCommand.php
+++ b/WebFiori/Framework/Cli/Commands/CreateCommand.php
@@ -15,9 +15,11 @@
use WebFiori\Framework\Cli\CLIUtils;
use WebFiori\Framework\Cli\Helpers\ClassInfoReader;
use WebFiori\Framework\Cli\Helpers\CreateAPITestCase;
+use WebFiori\Framework\Cli\Helpers\CreateAttributeTable;
use WebFiori\Framework\Cli\Helpers\CreateBackgroundTask;
use WebFiori\Framework\Cli\Helpers\CreateCLIClassHelper;
use WebFiori\Framework\Cli\Helpers\CreateDBAccessHelper;
+use WebFiori\Framework\Cli\Helpers\CreateDomainEntity;
use WebFiori\Framework\Cli\Helpers\CreateFullRESTHelper;
use WebFiori\Framework\Cli\Helpers\CreateMiddleware;
use WebFiori\Framework\Cli\Helpers\CreateMigration;
@@ -82,8 +84,14 @@ public function exec() : int {
} else if ($answer == 'Database table class.') {
$create = new CreateTableObj($this);
$create->readClassInfo();
+ } else if ($answer == 'Attribute-based table schema (Clean Architecture).') {
+ $create = new CreateAttributeTable($this);
+ $create->writeClass();
} else if ($answer == 'Entity class from table.') {
$this->createEntityFromQuery();
+ } else if ($answer == 'Pure domain entity (Clean Architecture).') {
+ $create = new CreateDomainEntity($this);
+ $create->writeClass();
} else if ($answer == 'Web service.') {
$create = new CreateWebService($this);
$create->readClassInfo();
@@ -127,7 +135,9 @@ public function exec() : int {
private function getWhat() {
$options = [];
$options['table'] = 'Database table class.';
+ $options['table-attributes'] = 'Attribute-based table schema (Clean Architecture).';
$options['entity'] = 'Entity class from table.';
+ $options['domain-entity'] = 'Pure domain entity (Clean Architecture).';
$options['web-service'] = 'Web service.';
$options['task'] = 'Background Task.';
$options['middleware'] = 'Middleware.';
diff --git a/WebFiori/Framework/Cli/Helpers/CreateAttributeTable.php b/WebFiori/Framework/Cli/Helpers/CreateAttributeTable.php
new file mode 100644
index 000000000..8dc15a48c
--- /dev/null
+++ b/WebFiori/Framework/Cli/Helpers/CreateAttributeTable.php
@@ -0,0 +1,77 @@
+isArgProvided('--defaults')) {
+ $ns = $this->getCommand()->getInput('Table schema namespace: Enter = \''.$ns.'\'') ?: $ns;
+ }
+
+ $this->setNamespace($ns);
+ $this->setClassName($command->readClassName('Enter table class name:', 'Table'));
+
+ $tableName = $this->getInput('Enter database table name:');
+ $this->getWriter()->setTableName($tableName);
+
+ if (!$command->isArgProvided('--defaults')) {
+ $this->readColumns();
+ }
+ }
+
+ private function readColumns() {
+ $this->println('Add columns to the table:');
+
+ while (true) {
+ $name = $this->getInput('Column name (or press Enter to finish):');
+ if (empty($name)) {
+ break;
+ }
+
+ $type = $this->select('Column type:', [
+ 'INT', 'VARCHAR', 'TEXT', 'DATETIME', 'TIMESTAMP', 'BOOL', 'DOUBLE', 'DECIMAL'
+ ], 1);
+
+ $options = [];
+
+ if ($type === 'VARCHAR' || $type === 'DECIMAL') {
+ $size = (int)$this->getInput('Size:', $type === 'VARCHAR' ? '255' : '10');
+ $options['size'] = $size;
+ }
+
+ if ($this->confirm('Is primary key?', false)) {
+ $options['primary'] = true;
+ if ($type === 'INT' && $this->confirm('Auto increment?', true)) {
+ $options['autoIncrement'] = true;
+ }
+ }
+
+ if ($this->confirm('Is nullable?', false)) {
+ $options['nullable'] = true;
+ }
+
+ $this->getWriter()->addColumn($name, $type, $options);
+ $this->success("Added column: $name");
+ }
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateAttributeTableTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateAttributeTableTest.php
new file mode 100644
index 000000000..8debc52cf
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateAttributeTableTest.php
@@ -0,0 +1,98 @@
+cleanupInfrastructure();
+ parent::tearDown();
+ }
+
+ private function cleanupInfrastructure(): void {
+ $dir = APP_PATH . 'Infrastructure' . DS . 'Schema';
+ if (is_dir($dir)) {
+ foreach (glob($dir . DS . '*.php') as $file) {
+ unlink($file);
+ }
+ }
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateAttributeTable() {
+ $output = $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'table-attributes'
+ ], [
+ "\n", // namespace (use default)
+ 'UsersTable', // class name
+ 'users', // table name
+ 'id', // column name
+ '0', // type: INT
+ '11', // size
+ 'y', // is primary
+ 'y', // auto increment
+ 'n', // not nullable
+ 'name', // column name
+ '1', // type: VARCHAR
+ '100', // size
+ 'n', // not primary
+ 'n', // not nullable
+ 'email', // column name
+ '1', // type: VARCHAR
+ '150', // size
+ 'n', // not primary
+ 'n', // not nullable
+ "\n" // finish
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $filePath = APP_PATH . 'Infrastructure' . DS . 'Schema' . DS . 'UsersTable.php';
+ $this->assertTrue(file_exists($filePath), 'Table schema file should exist');
+
+ $content = file_get_contents($filePath);
+ $this->assertStringContainsString('namespace App\\Infrastructure\\Schema', $content);
+ $this->assertStringContainsString('use WebFiori\\Database\\Attributes\\Table', $content);
+ $this->assertStringContainsString('use WebFiori\\Database\\Attributes\\Column', $content);
+ $this->assertStringContainsString('#[Table(name: \'users\')]', $content);
+ $this->assertStringContainsString('#[Column(name: \'id\'', $content);
+ $this->assertStringContainsString('primary: true', $content);
+ $this->assertStringContainsString('autoIncrement: true', $content);
+ $this->assertStringContainsString('#[Column(name: \'name\'', $content);
+ $this->assertStringContainsString('#[Column(name: \'email\'', $content);
+ $this->assertStringContainsString('class UsersTable', $content);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateAttributeTableWithDefaults() {
+ $output = $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'table-attributes',
+ '--defaults' => ''
+ ], [
+ 'ProductsTable',
+ 'products'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $filePath = APP_PATH . 'Infrastructure' . DS . 'Schema' . DS . 'ProductsTable.php';
+ $this->assertTrue(file_exists($filePath));
+
+ $content = file_get_contents($filePath);
+ $this->assertStringContainsString('class ProductsTable', $content);
+ $this->assertStringContainsString('#[Table(name: \'products\')]', $content);
+ }
+}
From 71d34ec99a04d497acc2dbf9d05764497aa2297a Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 18:20:45 +0300
Subject: [PATCH 15/88] Create CreateRestService.php
---
.../Cli/Helpers/CreateRestService.php | 100 ++++++++++++++++++
1 file changed, 100 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Helpers/CreateRestService.php
diff --git a/WebFiori/Framework/Cli/Helpers/CreateRestService.php b/WebFiori/Framework/Cli/Helpers/CreateRestService.php
new file mode 100644
index 000000000..572cb2524
--- /dev/null
+++ b/WebFiori/Framework/Cli/Helpers/CreateRestService.php
@@ -0,0 +1,100 @@
+isArgProvided('--defaults')) {
+ $ns = $this->getCommand()->getInput('Service namespace: Enter = \''.$ns.'\'') ?: $ns;
+ }
+
+ $this->setNamespace($ns);
+ $this->setClassName($command->readClassName('Enter service class name:', 'Service'));
+
+ $description = $this->getInput('Service description:');
+ $this->getWriter()->setDescription($description);
+
+ if (!$command->isArgProvided('--defaults')) {
+ $this->readMethods();
+ }
+ }
+
+ private function readMethods() {
+ $this->println('Add HTTP methods to the service:');
+
+ while (true) {
+ $httpMethod = $this->select('HTTP method (or select Cancel):', ['GET', 'POST', 'PUT', 'DELETE', 'Cancel'], 0);
+
+ if ($httpMethod === 'Cancel') {
+ break;
+ }
+
+ $methodName = $this->getInput('Method name:');
+ $params = [];
+
+ if ($this->confirm('Add parameters?', false)) {
+ $params = $this->readParameters();
+ }
+
+ $returnType = $this->select('Return type:', ['array', 'string', 'int', 'bool'], 0);
+
+ $this->getWriter()->addMethod($httpMethod, $methodName, $params, $returnType);
+ $this->success("Added method: $methodName");
+
+ if (!$this->confirm('Add another method?', false)) {
+ break;
+ }
+ }
+ }
+
+ private function readParameters(): array {
+ $params = [];
+
+ while (true) {
+ $name = $this->getInput('Parameter name (or press Enter to finish):');
+ if (empty($name)) {
+ break;
+ }
+
+ $type = $this->select('Parameter type:', ['STRING', 'INT', 'DOUBLE', 'BOOL', 'EMAIL', 'URL', 'ARRAY'], 0);
+ $description = $this->getInput('Parameter description:');
+
+ $param = [
+ 'name' => $name,
+ 'type' => $type,
+ 'description' => $description
+ ];
+
+ if ($type === 'INT' || $type === 'DOUBLE') {
+ if ($this->confirm('Set min/max values?', false)) {
+ $param['min'] = (int)$this->getInput('Minimum value:');
+ $param['max'] = (int)$this->getInput('Maximum value:');
+ }
+ }
+
+ $params[] = $param;
+ $this->success("Added parameter: $name");
+ }
+
+ return $params;
+ }
+}
From 2a88eb70eb3915685a62df724f7d8d238a927f91 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 18:21:05 +0300
Subject: [PATCH 16/88] Update RepositoryWriter.php
---
WebFiori/Framework/Writers/RepositoryWriter.php | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/WebFiori/Framework/Writers/RepositoryWriter.php b/WebFiori/Framework/Writers/RepositoryWriter.php
index fc33c12a5..85611659c 100644
--- a/WebFiori/Framework/Writers/RepositoryWriter.php
+++ b/WebFiori/Framework/Writers/RepositoryWriter.php
@@ -66,23 +66,23 @@ public function writeClassDeclaration() {
}
private function writeGetTableName() {
- $this->append($this->f('getTableName', [], 'string').' {', 1);
- $this->append("return '{$this->tableName}';", 2);
+ $this->append($this->f('getTableName', [], 'string'), 1);
+ $this->append('return \''.$this->tableName.'\';', 2);
$this->append('}', 1);
$this->append('', 1);
}
private function writeGetIdField() {
- $this->append($this->f('getIdField', [], 'string').' {', 1);
- $this->append("return '{$this->idField}';", 2);
+ $this->append($this->f('getIdField', [], 'string'), 1);
+ $this->append('return \''.$this->idField.'\';', 2);
$this->append('}', 1);
$this->append('', 1);
}
private function writeToEntity() {
$entityShortName = basename(str_replace('\\', '/', $this->entityClass));
- $this->append($this->f('toEntity', ['row' => 'array'], $entityShortName).' {', 1);
- $this->append("return new $entityShortName(", 2);
+ $this->append($this->f('toEntity', ['row' => 'array'], $entityShortName), 1);
+ $this->append('return new '.$entityShortName.'(', 2);
$params = [];
foreach ($this->properties as $prop) {
@@ -97,7 +97,7 @@ private function writeToEntity() {
}
private function writeToArray() {
- $this->append($this->f('toArray', ['entity' => 'object'], 'array').' {', 1);
+ $this->append($this->f('toArray', ['entity' => 'object'], 'array'), 1);
$this->append('return [', 2);
foreach ($this->properties as $prop) {
From 980209f90a070a0bd972a438e78dcd83d496ceba Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 18:21:39 +0300
Subject: [PATCH 17/88] Create CreateRepository.php
---
.../Cli/Helpers/CreateRepository.php | 62 +++++++++++++++++++
1 file changed, 62 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Helpers/CreateRepository.php
diff --git a/WebFiori/Framework/Cli/Helpers/CreateRepository.php b/WebFiori/Framework/Cli/Helpers/CreateRepository.php
new file mode 100644
index 000000000..850550158
--- /dev/null
+++ b/WebFiori/Framework/Cli/Helpers/CreateRepository.php
@@ -0,0 +1,62 @@
+isArgProvided('--defaults')) {
+ $ns = $this->getCommand()->getInput('Repository namespace: Enter = \''.$ns.'\'') ?: $ns;
+ }
+
+ $this->setNamespace($ns);
+ $this->setClassName($command->readClassName('Enter repository class name:', 'Repository'));
+
+ $entityClass = $this->getInput('Enter entity class (e.g., App\\Domain\\User):');
+ $this->getWriter()->setEntityClass($entityClass);
+
+ $tableName = $this->getInput('Enter table name:');
+ $this->getWriter()->setTableName($tableName);
+
+ $idField = $this->getInput('Enter ID field name:', 'id');
+ $this->getWriter()->setIdField($idField);
+
+ if (!$command->isArgProvided('--defaults')) {
+ $this->readProperties();
+ }
+ }
+
+ private function readProperties() {
+ $this->println('Add entity properties (for mapping):');
+
+ while (true) {
+ $name = $this->getInput('Property name (or press Enter to finish):');
+ if (empty($name)) {
+ break;
+ }
+
+ $type = $this->select('Property type:', ['int', 'string', 'bool', 'float'], 1);
+
+ $this->getWriter()->addProperty($name, $type);
+ $this->success("Added property: $name");
+ }
+ }
+}
From 74b6959e831b1e7b796af4bc0b2b0cdb2c4e9fdb Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 18:21:56 +0300
Subject: [PATCH 18/88] Update CreateCommand.php
---
WebFiori/Framework/Cli/Commands/CreateCommand.php | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/WebFiori/Framework/Cli/Commands/CreateCommand.php b/WebFiori/Framework/Cli/Commands/CreateCommand.php
index 744000a5a..642a5e019 100644
--- a/WebFiori/Framework/Cli/Commands/CreateCommand.php
+++ b/WebFiori/Framework/Cli/Commands/CreateCommand.php
@@ -18,11 +18,14 @@
use WebFiori\Framework\Cli\Helpers\CreateAttributeTable;
use WebFiori\Framework\Cli\Helpers\CreateBackgroundTask;
use WebFiori\Framework\Cli\Helpers\CreateCLIClassHelper;
+use WebFiori\Framework\Cli\Helpers\CreateCleanArchStack;
use WebFiori\Framework\Cli\Helpers\CreateDBAccessHelper;
use WebFiori\Framework\Cli\Helpers\CreateDomainEntity;
use WebFiori\Framework\Cli\Helpers\CreateFullRESTHelper;
use WebFiori\Framework\Cli\Helpers\CreateMiddleware;
use WebFiori\Framework\Cli\Helpers\CreateMigration;
+use WebFiori\Framework\Cli\Helpers\CreateRepository;
+use WebFiori\Framework\Cli\Helpers\CreateRestService;
use WebFiori\Framework\Cli\Helpers\CreateTableObj;
use WebFiori\Framework\Cli\Helpers\CreateThemeHelper;
use WebFiori\Framework\Cli\Helpers\CreateWebService;
@@ -95,6 +98,9 @@ public function exec() : int {
} else if ($answer == 'Web service.') {
$create = new CreateWebService($this);
$create->readClassInfo();
+ } else if ($answer == 'Annotation-based REST service (Clean Architecture).') {
+ $create = new CreateRestService($this);
+ $create->writeClass();
} else if ($answer == 'Middleware.') {
$create = new CreateMiddleware($this);
$create->readClassInfo();
@@ -116,6 +122,12 @@ public function exec() : int {
$create->readEntityInfo();
$create->confirnIncludeColsUpdate();
$create->writeClass();
+ } else if ($answer == 'Repository class (Clean Architecture).') {
+ $create = new CreateRepository($this);
+ $create->writeClass();
+ } else if ($answer == 'Complete clean architecture stack (Entity + Table + Repository).') {
+ $create = new CreateCleanArchStack($this);
+ return $create->writeClasses();
} else if ($answer == 'Complete REST backend (Database table, entity, database access and web services).') {
$create = new CreateFullRESTHelper($this);
$create->readInfo();
@@ -139,11 +151,14 @@ private function getWhat() {
$options['entity'] = 'Entity class from table.';
$options['domain-entity'] = 'Pure domain entity (Clean Architecture).';
$options['web-service'] = 'Web service.';
+ $options['rest-service'] = 'Annotation-based REST service (Clean Architecture).';
$options['task'] = 'Background Task.';
$options['middleware'] = 'Middleware.';
$options['command'] = 'CLI Command.';
$options['theme'] = 'Theme.';
$options['db'] = 'Database access class based on table.';
+ $options['repository'] = 'Repository class (Clean Architecture).';
+ $options['clean-stack'] = 'Complete clean architecture stack (Entity + Table + Repository).';
$options['rest'] = 'Complete REST backend (Database table, entity, database access and web services).';
$options['api-test'] = 'Web service test case.';
$options['migration'] = 'Database migration.';
From d14217474ff5f300ea0b174e523c321dff736490 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 18:22:30 +0300
Subject: [PATCH 19/88] Create CreateDomainEntityTest.php
---
.../Tests/Cli/CreateDomainEntityTest.php | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateDomainEntityTest.php
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateDomainEntityTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateDomainEntityTest.php
new file mode 100644
index 000000000..d66a3ae41
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateDomainEntityTest.php
@@ -0,0 +1,93 @@
+cleanupDomain();
+ parent::tearDown();
+ }
+
+ private function cleanupDomain(): void {
+ $dir = APP_PATH . 'Domain';
+ if (is_dir($dir)) {
+ foreach (glob($dir . DS . '*.php') as $file) {
+ unlink($file);
+ }
+ }
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateDomainEntity() {
+ $output = $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'domain-entity'
+ ], [
+ "\n", // namespace (use default)
+ 'User', // class name
+ 'id', // property name
+ '0', // type: int
+ 'y', // nullable
+ 'name', // property name
+ '1', // type: string
+ 'n', // not nullable
+ 'email', // property name
+ '1', // type: string
+ 'n', // not nullable
+ "\n" // finish
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $filePath = APP_PATH . 'Domain' . DS . 'User.php';
+ $this->assertTrue(file_exists($filePath), 'Entity file should exist');
+
+ $content = file_get_contents($filePath);
+ $this->assertStringContainsString('namespace App\\Domain', $content);
+ $this->assertStringContainsString('class User', $content);
+ $this->assertStringContainsString('public ?int $id', $content);
+ $this->assertStringContainsString('public string $name', $content);
+ $this->assertStringContainsString('public string $email', $content);
+
+ // Test that class can be loaded
+ require_once $filePath;
+ $this->assertTrue(class_exists('\\App\\Domain\\User'));
+
+ // Test instantiation
+ $user = new \App\Domain\User(1, 'John Doe', 'john@example.com');
+ $this->assertEquals(1, $user->id);
+ $this->assertEquals('John Doe', $user->name);
+ $this->assertEquals('john@example.com', $user->email);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateDomainEntityWithDefaults() {
+ $output = $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'domain-entity',
+ '--defaults' => ''
+ ], [
+ 'Product'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $filePath = APP_PATH . 'Domain' . DS . 'Product.php';
+ $this->assertTrue(file_exists($filePath));
+
+ $content = file_get_contents($filePath);
+ $this->assertStringContainsString('class Product', $content);
+ }
+}
From f00a100fa7f39d69c713138fd16819c0b2be8a08 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 18:22:46 +0300
Subject: [PATCH 20/88] Create CreateRepositoryTest.php
---
.../Tests/Cli/CreateRepositoryTest.php | 93 +++++++++++++++++++
1 file changed, 93 insertions(+)
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateRepositoryTest.php
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryTest.php
new file mode 100644
index 000000000..c6398100d
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryTest.php
@@ -0,0 +1,93 @@
+cleanupInfrastructure();
+ parent::tearDown();
+ }
+
+ private function cleanupInfrastructure(): void {
+ $dir = APP_PATH . 'Infrastructure' . DS . 'Repository';
+ if (is_dir($dir)) {
+ foreach (glob($dir . DS . '*.php') as $file) {
+ unlink($file);
+ }
+ }
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateRepository() {
+ $output = $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'repository'
+ ], [
+ "\n", // namespace (use default)
+ 'UserRepository', // class name
+ 'App\\Domain\\User', // entity class
+ 'users', // table name
+ 'id', // id field
+ 'id', // property name
+ '0', // type: int
+ 'name', // property name
+ '1', // type: string
+ 'email', // property name
+ '1', // type: string
+ "\n" // finish
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $filePath = APP_PATH . 'Infrastructure' . DS . 'Repository' . DS . 'UserRepository.php';
+ $this->assertTrue(file_exists($filePath), 'Repository file should exist');
+
+ $content = file_get_contents($filePath);
+ $this->assertStringContainsString('namespace App\\Infrastructure\\Repository', $content);
+ $this->assertStringContainsString('use WebFiori\\Database\\Repository\\AbstractRepository', $content);
+ $this->assertStringContainsString('use App\\Domain\\User', $content);
+ $this->assertStringContainsString('class UserRepository extends AbstractRepository', $content);
+ $this->assertStringContainsString('function getTableName()', $content);
+ $this->assertStringContainsString('return \'users\'', $content);
+ $this->assertStringContainsString('function getIdField()', $content);
+ $this->assertStringContainsString('return \'id\'', $content);
+ $this->assertStringContainsString('function toEntity', $content);
+ $this->assertStringContainsString('function toArray', $content);
+ $this->assertStringContainsString('new User(', $content);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateRepositoryWithDefaults() {
+ $output = $this->executeMultiCommand([
+ CreateCommand::class,
+ '--c' => 'repository',
+ '--defaults' => ''
+ ], [
+ 'ProductRepository',
+ 'App\\Domain\\Product',
+ 'products',
+ 'id'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+
+ $filePath = APP_PATH . 'Infrastructure' . DS . 'Repository' . DS . 'ProductRepository.php';
+ $this->assertTrue(file_exists($filePath));
+
+ $content = file_get_contents($filePath);
+ $this->assertStringContainsString('class ProductRepository', $content);
+ $this->assertStringContainsString('return \'products\'', $content);
+ }
+}
From eee51caff8a3b09c3920d3bf6837a0e53cf29eb3 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 22:44:53 +0300
Subject: [PATCH 21/88] refactor: How to Locate `ClassLoader`
---
WebFiori/Framework/App.php | 35 ++++++++++++++++++++---------------
1 file changed, 20 insertions(+), 15 deletions(-)
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index df2feecc0..627a267d4 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -384,11 +384,14 @@ public static function initiate(string $appFolder = 'App', string $publicFolder
*/
define('PUBLIC_FOLDER', $publicFolder);
}
- if (!defined('WF_CORE_PATH')) {
+ if (!defined('WF_CORE_PATHS')) {
/**
- * Path to WebFiori's core library.
+ * Possible Paths to WebFiori's core library.
*/
- define('WF_CORE_PATH', ROOT_PATH.DS.'vendor'.DS.'webfiori'.DS.'framework'.DS.'WebFiori'.DS.'Framework');
+ define('WF_CORE_PATHS', [
+ ROOT_PATH.DS.'vendor'.DS.'webfiori'.DS.'framework'.DS.'WebFiori'.DS.'Framework',
+ ROOT_PATH.DS.'WebFiori'.DS.'Framework'
+ ]);
}
self::initAutoLoader();
self::checkStandardLibs();
@@ -649,20 +652,22 @@ private static function initAutoLoader() {
* Initialize autoloader.
*/
if (!class_exists('WebFiori\Framework\Autoload\ClassLoader',false)) {
- $autoloader = WF_CORE_PATH.DIRECTORY_SEPARATOR.'Autoload'.DIRECTORY_SEPARATOR.'ClassLoader.php';
-
- if (!file_exists($autoloader)) {
- throw new \Exception('Unable to locate the autoloader class.');
- }
- require_once $autoloader;
- }
- self::$AU = ClassLoader::get();
+ foreach (WF_CORE_PATHS as $path) {
+ $autoloader = $path.DIRECTORY_SEPARATOR.'Autoload'.DIRECTORY_SEPARATOR.'ClassLoader.php';
- if (!class_exists(APP_DIR.'\\Init\\InitAutoLoad')) {
- Ini::createAppDirs();
- Ini::get()->createIniClass('InitAutoLoad', 'Add user-defined directories to the set of directories at which the framework will search for classes.');
+ if (file_exists($autoloader)) {
+ require_once $autoloader;
+ self::$AU = ClassLoader::get();
+ }
+ if (!class_exists(APP_DIR.'\\Init\\InitAutoLoad')) {
+ Ini::createAppDirs();
+ Ini::get()->createIniClass('InitAutoLoad', 'Add user-defined directories to the set of directories at which the framework will search for classes.');
+ }
+ self::call(APP_DIR.'\\Init\\InitAutoLoad::init');
+ return;
+ }
}
- self::call(APP_DIR.'\\Init\\InitAutoLoad::init');
+ throw new \Exception('Unable to locate the autoloader class.');
}
/**
* Initialize global constants which has information about framework version.
From d505785653d59f9ae237ee70cdf0912c4800c336 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 22:45:51 +0300
Subject: [PATCH 22/88] Update .gitignore
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index 713406857..e1e97f9d9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,4 @@ App/Config/*
*.Identifier
tests/clover.xml
/.vscode
+/App
From 153732cf28cc22251356c67ba35154b2a3b02c7c Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 23:06:57 +0300
Subject: [PATCH 23/88] refactor: Use of Lines Instead of String Concat
---
WebFiori/Framework/Writers/ClassWriter.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index ed7675442..362c3cf96 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -27,7 +27,7 @@ abstract class ClassWriter {
*
* @since 1.0
*/
- private $classAsStr;
+ private $classLines;
/**
* The name of the class that will be created.
*
@@ -405,13 +405,13 @@ public function setSuffix(string $classNameSuffix) : bool {
public function writeClass() {
$classFile = new File($this->getName().'.php', $this->getPath());
$classFile->remove();
- $this->classAsStr = '';
+ $this->classLines = [];
$this->writeNsDeclaration();
$this->writeUseStatements();
$this->writeClassComment();
$this->writeClassDeclaration();
$this->writeClassBody();
- $classFile->setRawData($this->classAsStr);
+ $classFile->setRawData(implode("\n", $this->classLines));
$classFile->write(false, true);
}
public abstract function writeClassBody();
@@ -446,7 +446,7 @@ public function writeUseStatements() {
}
private function a($str, $tapsCount) {
$tabStr = str_repeat(' ', $tapsCount);
- $this->classAsStr .= $tabStr.$str."\n";
+ $this->classLines[] = $tabStr.$str;
}
private function fixClassName($className) {
$classSuffix = $this->getSuffix();
From 18da8fad4602018a62724463acf587a682ed4773 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 23:14:02 +0300
Subject: [PATCH 24/88] refactor: Added Code Normalization
---
WebFiori/Framework/Writers/ClassWriter.php | 20 ++++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index 362c3cf96..b7c57de3f 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -411,7 +411,7 @@ public function writeClass() {
$this->writeClassComment();
$this->writeClassDeclaration();
$this->writeClassBody();
- $classFile->setRawData(implode("\n", $this->classLines));
+ $classFile->setRawData(implode("\n", $this->normalizeCode($this->classLines)));
$classFile->write(false, true);
}
public abstract function writeClassBody();
@@ -448,7 +448,23 @@ private function a($str, $tapsCount) {
$tabStr = str_repeat(' ', $tapsCount);
$this->classLines[] = $tabStr.$str;
}
- private function fixClassName($className) {
+ private function normalizeCode(array $lines) : array {
+ $normalized = [];
+ $prevLineEmpty = false;
+
+ foreach ($lines as $line) {
+ $isEmpty = trim($line) === '';
+
+ if ($isEmpty && $prevLineEmpty) {
+ continue;
+ }
+
+ $normalized[] = $line;
+ $prevLineEmpty = $isEmpty;
+ }
+
+ return $normalized;
+ } private function fixClassName($className) {
$classSuffix = $this->getSuffix();
if ($classSuffix == '') {
From 4d486b27446d6d6e53a0d2e54a23e09b8d6e40bc Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 23:25:43 +0300
Subject: [PATCH 25/88] feat: Add New Method" `method`
---
WebFiori/Framework/Writers/ClassWriter.php | 58 ++++++++++++++++++----
1 file changed, 48 insertions(+), 10 deletions(-)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index b7c57de3f..0d9d23f48 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -139,24 +139,62 @@ public function append($strOrArr, $tabsCount = 0) {
* it.
*/
public function f($funcName, $argsArr = [], ?string $returns = null) {
+ return $this->method($funcName, $argsArr, $returns);
+ }
+ /**
+ * Adds method definition with full control over modifiers.
+ *
+ * @param string $funcName Method name
+ * @param array $argsArr Arguments [name => type]
+ * @param string|null $returns Return type
+ * @param string $visibility Visibility: 'public', 'protected', 'private'
+ * @param bool $isStatic Is static method
+ * @param bool $isAbstract Is abstract method
+ * @param bool $isFinal Is final method
+ *
+ * @return string Method signature
+ */
+ public function method(
+ string $funcName,
+ array $argsArr = [],
+ ?string $returns = null,
+ string $visibility = 'public',
+ bool $isStatic = false,
+ bool $isAbstract = false,
+ bool $isFinal = false
+ ) : string {
+ $modifiers = [];
+
+ if ($isFinal) {
+ $modifiers[] = 'final';
+ }
+ if ($isAbstract) {
+ $modifiers[] = 'abstract';
+ }
+
+ $modifiers[] = $visibility;
+
+ if ($isStatic) {
+ $modifiers[] = 'static';
+ }
+
+ $signature = implode(' ', $modifiers) . ' function ' . $funcName;
+
$argsPart = '(';
-
foreach ($argsArr as $argName => $argType) {
if (strlen($argsPart) != 1) {
- $argsPart .= ', '.$argType.' $'.$argName;
- continue;
+ $argsPart .= ', ';
}
- $argsPart .= $argType.' $'.$argName;
+ $argsPart .= $argType . ' $' . $argName;
}
$argsPart .= ')';
-
+
if ($returns !== null) {
- $argsPart .= ' : '.$returns;
+ $argsPart .= ' : ' . $returns;
}
-
- return 'public function '.$funcName.$argsPart.' {';
- }
- /**
+
+ return $signature . $argsPart . ($isAbstract ? ';' : ' {');
+ } /**
* Returns the absolute path of the class that will be created.
*
* @return string The absolute path of the file that holds class information.
From bebe151ebf59b7612b4f064c5dfddcd926e7fd82 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 23:30:26 +0300
Subject: [PATCH 26/88] feat: Add `property` and `constant`
---
WebFiori/Framework/Writers/ClassWriter.php | 61 +++++++++++++++++++++-
1 file changed, 60 insertions(+), 1 deletion(-)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index 0d9d23f48..c48b15bbf 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -194,7 +194,66 @@ public function method(
}
return $signature . $argsPart . ($isAbstract ? ';' : ' {');
- } /**
+ }
+ /**
+ * Generate a property declaration.
+ *
+ * @param string $name Property name
+ * @param string $visibility Visibility: 'public', 'protected', 'private'
+ * @param string|null $type Property type
+ * @param string|null $defaultValue Default value as string
+ * @param bool $isStatic Is static property
+ * @param bool $isReadonly Is readonly property (PHP 8.1+)
+ *
+ * @return string Property declaration
+ */
+ public function property(
+ string $name,
+ string $visibility = 'private',
+ ?string $type = null,
+ ?string $defaultValue = null,
+ bool $isStatic = false,
+ bool $isReadonly = false
+ ) : string {
+ $modifiers = [$visibility];
+
+ if ($isReadonly) {
+ $modifiers[] = 'readonly';
+ }
+ if ($isStatic) {
+ $modifiers[] = 'static';
+ }
+
+ $declaration = implode(' ', $modifiers);
+
+ if ($type !== null) {
+ $declaration .= ' ' . $type;
+ }
+
+ $declaration .= ' $' . $name;
+
+ if ($defaultValue !== null) {
+ $declaration .= ' = ' . $defaultValue;
+ }
+
+ return $declaration . ';';
+ }
+ /**
+ * Generate a constant declaration.
+ *
+ * @param string $name Constant name
+ * @param string $value Constant value as string
+ * @param string $visibility Visibility: 'public', 'protected', 'private'
+ *
+ * @return string Constant declaration
+ */
+ public function constant(
+ string $name,
+ string $value,
+ string $visibility = 'public'
+ ) : string {
+ return $visibility . ' const ' . $name . ' = ' . $value . ';';
+ }/**
* Returns the absolute path of the class that will be created.
*
* @return string The absolute path of the file that holds class information.
From 0eb7760ea9107f5a004f30e2b2d0b5526e3eb9ac Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 23:38:53 +0300
Subject: [PATCH 27/88] feat: Doc-block Builder
---
WebFiori/Framework/Writers/ClassWriter.php | 13 +-
.../Framework/Writers/DocblockBuilder.php | 158 ++++++++++++++++++
2 files changed, 170 insertions(+), 1 deletion(-)
create mode 100644 WebFiori/Framework/Writers/DocblockBuilder.php
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index c48b15bbf..c09113c64 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -253,7 +253,18 @@ public function constant(
string $visibility = 'public'
) : string {
return $visibility . ' const ' . $name . ' = ' . $value . ';';
- }/**
+ }
+ /**
+ * Start building a docblock.
+ *
+ * @param string $description Main description
+ *
+ * @return DocblockBuilder
+ */
+ public function docblock(string $description = '') : DocblockBuilder {
+ return new DocblockBuilder($this, $description);
+ }
+ /**
* Returns the absolute path of the class that will be created.
*
* @return string The absolute path of the file that holds class information.
diff --git a/WebFiori/Framework/Writers/DocblockBuilder.php b/WebFiori/Framework/Writers/DocblockBuilder.php
new file mode 100644
index 000000000..d763b7abe
--- /dev/null
+++ b/WebFiori/Framework/Writers/DocblockBuilder.php
@@ -0,0 +1,158 @@
+writer = $writer;
+ $this->description = $description;
+ }
+
+ /**
+ * Add a parameter to the docblock.
+ *
+ * @param string $type Parameter type
+ * @param string $name Parameter name (without $)
+ * @param string $desc Optional description
+ *
+ * @return DocblockBuilder
+ */
+ public function param(string $type, string $name, string $desc = '') : self {
+ $this->params[] = ['type' => $type, 'name' => $name, 'desc' => $desc];
+ return $this;
+ }
+
+ /**
+ * Add a return tag to the docblock.
+ *
+ * @param string $type Return type
+ * @param string $desc Optional description
+ *
+ * @return DocblockBuilder
+ */
+ public function returns(string $type, string $desc = '') : self {
+ $this->return = ['type' => $type, 'desc' => $desc];
+ return $this;
+ }
+
+ /**
+ * Add a custom tag to the docblock.
+ *
+ * @param string $name Tag name (without @)
+ * @param string $value Optional tag value
+ *
+ * @return DocblockBuilder
+ */
+ public function tag(string $name, string $value = '') : self {
+ $this->tags[] = ['name' => $name, 'value' => $value];
+ return $this;
+ }
+
+ /**
+ * Add @throws tag.
+ *
+ * @param string $exception Exception class name
+ * @param string $desc Optional description
+ *
+ * @return DocblockBuilder
+ */
+ public function throws(string $exception, string $desc = '') : self {
+ return $this->tag('throws', $exception . ($desc ? ' ' . $desc : ''));
+ }
+
+ /**
+ * Add @deprecated tag.
+ *
+ * @param string $message Optional deprecation message
+ *
+ * @return DocblockBuilder
+ */
+ public function deprecated(string $message = '') : self {
+ return $this->tag('deprecated', $message);
+ }
+
+ /**
+ * Add @since tag.
+ *
+ * @param string $version Version number
+ *
+ * @return DocblockBuilder
+ */
+ public function since(string $version) : self {
+ return $this->tag('since', $version);
+ }
+
+ /**
+ * Build and append the docblock to the class writer.
+ *
+ * @param int $indent Indentation level (number of tabs)
+ *
+ * @return array The generated docblock lines
+ */
+ public function build(int $indent = 1) : array {
+ $lines = ['/**'];
+
+ if ($this->description) {
+ foreach (explode("\n", $this->description) as $line) {
+ $lines[] = ' * ' . $line;
+ }
+ if (!empty($this->params) || $this->return || !empty($this->tags)) {
+ $lines[] = ' *';
+ }
+ }
+
+ foreach ($this->params as $param) {
+ $line = ' * @param ' . $param['type'] . ' $' . $param['name'];
+ if ($param['desc']) {
+ $line .= ' ' . $param['desc'];
+ }
+ $lines[] = $line;
+ }
+
+ if ($this->return) {
+ $line = ' * @return ' . $this->return['type'];
+ if ($this->return['desc']) {
+ $line .= ' ' . $this->return['desc'];
+ }
+ $lines[] = $line;
+ }
+
+ foreach ($this->tags as $tag) {
+ $line = ' * @' . $tag['name'];
+ if ($tag['value']) {
+ $line .= ' ' . $tag['value'];
+ }
+ $lines[] = $line;
+ }
+
+ $lines[] = ' */';
+
+ $this->writer->append($lines, $indent);
+ return $lines;
+ }
+}
From ff37c27169234495d3fe8abc1a318ca039521535 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 23:46:22 +0300
Subject: [PATCH 28/88] feat: Added `getCode`
---
WebFiori/Framework/Writers/ClassWriter.php | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index c09113c64..ee08bb004 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -513,16 +513,23 @@ public function setSuffix(string $classNameSuffix) : bool {
public function writeClass() {
$classFile = new File($this->getName().'.php', $this->getPath());
$classFile->remove();
+ $classFile->setRawData($this->getCode());
+ $classFile->write(false, true);
+ }
+ /**
+ * Generate the class code without writing to disk.
+ *
+ * @return string The generated class code
+ */
+ public function getCode() : string {
$this->classLines = [];
$this->writeNsDeclaration();
$this->writeUseStatements();
$this->writeClassComment();
$this->writeClassDeclaration();
$this->writeClassBody();
- $classFile->setRawData(implode("\n", $this->normalizeCode($this->classLines)));
- $classFile->write(false, true);
- }
- public abstract function writeClassBody();
+ return implode("\n", $this->normalizeCode($this->classLines));
+ } public abstract function writeClassBody();
/**
* Writes the top section of the class that contains class comment.
*/
From 9e11d2be039a04ea2b0fa6797cd463e03b146584 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 20 Jan 2026 23:56:45 +0300
Subject: [PATCH 29/88] feat: Attributes
---
WebFiori/Framework/Writers/ClassWriter.php | 89 ++++++++++++++++++++++
1 file changed, 89 insertions(+)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index ee08bb004..64591e300 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -264,6 +264,95 @@ public function constant(
public function docblock(string $description = '') : DocblockBuilder {
return new DocblockBuilder($this, $description);
}
+ /**
+ /**
+ * Add an attribute for a class.
+ *
+ * @param string $name Attribute name (without #)
+ * @param array $params Attribute parameters
+ * @param int $indent Indentation level
+ *
+ * @return $this For chaining
+ */
+ public function classAttribute(string $name, array $params = [], int $indent = 0) {
+ $this->append($this->formatAttribute($name, $params), $indent);
+ return $this;
+ }
+ /**
+ * Add an attribute for a property.
+ *
+ * @param string $name Attribute name (without #)
+ * @param array $params Attribute parameters
+ * @param int $indent Indentation level
+ *
+ * @return $this For chaining
+ */
+ public function propertyAttribute(string $name, array $params = [], int $indent = 1) {
+ $this->append($this->formatAttribute($name, $params), $indent);
+ return $this;
+ }
+ /**
+ * Add an attribute for a method.
+ *
+ * @param string $name Attribute name (without #)
+ * @param array $params Attribute parameters
+ * @param int $indent Indentation level
+ *
+ * @return $this For chaining
+ */
+ public function methodAttribute(string $name, array $params = [], int $indent = 1) {
+ $this->append($this->formatAttribute($name, $params), $indent);
+ return $this;
+ }
+ /**
+ * Format an attribute string.
+ *
+ * @param string $name Attribute name
+ * @param array $params Attribute parameters
+ *
+ * @return string Formatted attribute
+ */
+ private function formatAttribute(string $name, array $params = []) : string {
+ $attr = '#[' . $name;
+
+ if (!empty($params)) {
+ $args = [];
+ foreach ($params as $key => $value) {
+ if (is_int($key)) {
+ $args[] = $this->formatAttributeValue($value);
+ } else {
+ $args[] = $key . ': ' . $this->formatAttributeValue($value);
+ }
+ }
+ $attr .= '(' . implode(', ', $args) . ')';
+ }
+
+ $attr .= ']';
+ return $attr;
+ }
+ /**
+ * Format a value for attribute parameters.
+ *
+ * @param mixed $value The value to format
+ *
+ * @return string Formatted value
+ */
+ private function formatAttributeValue($value) : string {
+ if (is_string($value)) {
+ return "'" . addslashes($value) . "'";
+ }
+ if (is_bool($value)) {
+ return $value ? 'true' : 'false';
+ }
+ if (is_array($value)) {
+ $items = array_map([$this, 'formatAttributeValue'], $value);
+ return '[' . implode(', ', $items) . ']';
+ }
+ if (is_null($value)) {
+ return 'null';
+ }
+ return (string)$value;
+ }
/**
* Returns the absolute path of the class that will be created.
*
From 57656d7029baa4199e5f7b307d8bbea6a79767d2 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 21 Jan 2026 00:00:05 +0300
Subject: [PATCH 30/88] refactor: Throw Exceptions on Errors
---
WebFiori/Framework/Writers/ClassWriter.php | 46 ++++++++++++----------
1 file changed, 26 insertions(+), 20 deletions(-)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index 64591e300..28d958672 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -525,16 +525,18 @@ public function removeUseStatement(string $classToRemove) {
* @return boolean If the name is successfully set, the method will return true.
* Other than that, false is returned.
*/
- public function setClassName(string $name) : bool {
+ public function setClassName(string $name) {
$trimmed = trim($name);
- if (self::isValidClassName($trimmed)) {
- $this->className = $this->fixClassName($trimmed);
-
- return true;
+ if (!self::isValidClassName($trimmed)) {
+ throw new \InvalidArgumentException(
+ "Invalid class name '$name'. Class names must start with a letter or underscore, " .
+ "followed by letters, numbers, or underscores."
+ );
}
- return false;
+ $this->className = $this->fixClassName($trimmed);
+ return $this;
}
/**
@@ -549,11 +551,14 @@ public function setNamespace(string $namespace) {
$trimmed = trim($namespace, ' ');
if (!self::isValidNamespace($trimmed)) {
- return false;
+ throw new \InvalidArgumentException(
+ "Invalid namespace '$namespace'. Namespaces must contain valid PHP identifiers " .
+ "separated by backslashes."
+ );
}
+
$this->ns = $trimmed[0] == '\\' ? substr($trimmed, 1) : $trimmed;
-
- return true;
+ return $this;
}
/**
* Sets the location at which the class will be created on.
@@ -563,15 +568,15 @@ public function setNamespace(string $namespace) {
* @return boolean If the path is successfully set, the method will return true.
* Other than that, false is returned.
*/
- public function setPath(string $path) : bool {
+ public function setPath(string $path) {
$trimmed = trim($path);
if (strlen($trimmed) == 0) {
- return false;
+ throw new \InvalidArgumentException("Path cannot be empty.");
}
+
$this->path = str_replace('\\', DS, str_replace('/', DS, $trimmed));
-
- return true;
+ return $this;
}
/**
* Sets a string as a suffix to the class name.
@@ -581,15 +586,16 @@ public function setPath(string $path) : bool {
*
* @return bool If set, the method will return true. False otherises.
*/
- public function setSuffix(string $classNameSuffix) : bool {
- if (self::isValidClassName($classNameSuffix)) {
- $this->suffix = $classNameSuffix;
- $this->className = $this->fixClassName($this->className);
-
- return true;
+ public function setSuffix(string $classNameSuffix) {
+ if (!self::isValidClassName($classNameSuffix)) {
+ throw new \InvalidArgumentException(
+ "Invalid suffix '$classNameSuffix'. Suffix must be a valid class name."
+ );
}
- return false;
+ $this->suffix = $classNameSuffix;
+ $this->className = $this->fixClassName($this->className);
+ return $this;
}
/**
* Write the new class to a .php file.
From f54ed91850136e334b6092e27c797ace2cf6a63c Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 21 Jan 2026 00:02:02 +0300
Subject: [PATCH 31/88] feat: Code Reuse Helpers
---
WebFiori/Framework/Writers/ClassWriter.php | 108 +++++++++++++++++++++
1 file changed, 108 insertions(+)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index 28d958672..930592f10 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -354,6 +354,114 @@ private function formatAttributeValue($value) : string {
return (string)$value;
}
/**
+ * Write a standard constructor method.
+ *
+ * @param array $params Constructor parameters [name => type]
+ * @param string|array $body Constructor body (string or array of lines)
+ * @param string $description Optional docblock description
+ * @param int $indent Indentation level
+ */
+ protected function writeConstructor(
+ array $params = [],
+ $body = '',
+ string $description = 'Creates new instance of the class.',
+ int $indent = 1
+ ) {
+ $this->docblock($description)->build($indent);
+ $this->append($this->method('__construct', $params), $indent);
+
+ if (is_array($body)) {
+ $this->append($body, $indent + 1);
+ } else if ($body) {
+ $this->append($body, $indent + 1);
+ }
+
+ $this->append('}', $indent);
+ }
+ /**
+ * Write a standard getter method.
+ *
+ * @param string $property Property name
+ * @param string $type Return type
+ * @param string $description Optional description
+ * @param int $indent Indentation level
+ */
+ protected function writeGetter(
+ string $property,
+ string $type,
+ string $description = '',
+ int $indent = 1
+ ) {
+ $methodName = 'get' . ucfirst($property);
+
+ $this->docblock($description ?: "Returns the value of $property.")
+ ->returns($type)
+ ->build($indent);
+
+ $this->append($this->method($methodName, [], $type), $indent);
+ $this->append("return \$this->$property;", $indent + 1);
+ $this->append('}', $indent);
+ }
+ /**
+ * Write a standard setter method.
+ *
+ * @param string $property Property name
+ * @param string $type Parameter type
+ * @param string $description Optional description
+ * @param int $indent Indentation level
+ */
+ protected function writeSetter(
+ string $property,
+ string $type,
+ string $description = '',
+ int $indent = 1
+ ) {
+ $methodName = 'set' . ucfirst($property);
+
+ $this->docblock($description ?: "Sets the value of $property.")
+ ->param($type, $property)
+ ->returns('void')
+ ->build($indent);
+
+ $this->append($this->method($methodName, [$property => $type], 'void'), $indent);
+ $this->append("\$this->$property = \$$property;", $indent + 1);
+ $this->append('}', $indent);
+ }
+ /**
+ * Write both getter and setter for a property.
+ *
+ * @param string $property Property name
+ * @param string $type Property type
+ * @param int $indent Indentation level
+ */
+ protected function writeGetterSetter(string $property, string $type, int $indent = 1) {
+ $this->writeGetter($property, $type, '', $indent);
+ $this->writeSetter($property, $type, '', $indent);
+ }
+ /**
+ * Write an empty method stub with TODO comment.
+ *
+ * @param string $methodName Method name
+ * @param array $params Method parameters [name => type]
+ * @param string|null $returns Return type
+ * @param string $description Method description
+ * @param int $indent Indentation level
+ */
+ protected function writeMethodStub(
+ string $methodName,
+ array $params = [],
+ ?string $returns = null,
+ string $description = '',
+ int $indent = 1
+ ) {
+ if ($description) {
+ $this->docblock($description)->build($indent);
+ }
+
+ $this->append($this->method($methodName, $params, $returns), $indent);
+ $this->append('//TODO: Implement this method.', $indent + 1);
+ $this->append('}', $indent);
+ } /**
* Returns the absolute path of the class that will be created.
*
* @return string The absolute path of the file that holds class information.
From d3b24077f1652396226152581f043d8bc323c15a Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 21 Jan 2026 00:13:32 +0300
Subject: [PATCH 32/88] feat: Chaining
---
WebFiori/Framework/Writers/ClassWriter.php | 38 +++++++++++++++-------
1 file changed, 27 insertions(+), 11 deletions(-)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index 930592f10..29b694e32 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -102,6 +102,8 @@ public function addUseStatement($classesToUse) {
}
/**
* Appends a string or array of strings to the string that represents the
+
+ return $this;
* body of the class.
*
* @param string $strOrArr The string that will be appended. At the end of the string
@@ -116,12 +118,13 @@ public function append($strOrArr, $tabsCount = 0) {
if (gettype($strOrArr) != 'array') {
$this->a($strOrArr, $tabsCount);
- return;
+ return $this;
}
foreach ($strOrArr as $str) {
$this->a($str, $tabsCount);
}
+ return $this;
}
/**
* Adds method definition to the class.
@@ -152,7 +155,7 @@ public function f($funcName, $argsArr = [], ?string $returns = null) {
* @param bool $isAbstract Is abstract method
* @param bool $isFinal Is final method
*
- * @return string Method signature
+ * @return $this For chaining
*/
public function method(
string $funcName,
@@ -162,7 +165,7 @@ public function method(
bool $isStatic = false,
bool $isAbstract = false,
bool $isFinal = false
- ) : string {
+ ) {
$modifiers = [];
if ($isFinal) {
@@ -193,7 +196,8 @@ public function method(
$argsPart .= ' : ' . $returns;
}
- return $signature . $argsPart . ($isAbstract ? ';' : ' {');
+ $this->append($signature . $argsPart . ($isAbstract ? ';' : ' {'), 1);
+ return $this;
}
/**
* Generate a property declaration.
@@ -205,7 +209,9 @@ public function method(
* @param bool $isStatic Is static property
* @param bool $isReadonly Is readonly property (PHP 8.1+)
*
- * @return string Property declaration
+ * @param int|null $indent If provided, appends to class and returns $this for chaining
+ *
+ * @return string|$this Property declaration string, or $this if $indent is provided
*/
public function property(
string $name,
@@ -214,7 +220,7 @@ public function property(
?string $defaultValue = null,
bool $isStatic = false,
bool $isReadonly = false
- ) : string {
+ ) {
$modifiers = [$visibility];
if ($isReadonly) {
@@ -236,7 +242,8 @@ public function property(
$declaration .= ' = ' . $defaultValue;
}
- return $declaration . ';';
+ $this->append($declaration . ';', 1);
+ return $this;
}
/**
* Generate a constant declaration.
@@ -245,14 +252,24 @@ public function property(
* @param string $value Constant value as string
* @param string $visibility Visibility: 'public', 'protected', 'private'
*
- * @return string Constant declaration
+ * @return $this For chaining
*/
public function constant(
string $name,
string $value,
string $visibility = 'public'
- ) : string {
- return $visibility . ' const ' . $name . ' = ' . $value . ';';
+ ) {
+ $this->append($visibility . ' const ' . $name . ' = ' . $value . ';', 1);
+ return $this;
+ }
+ /**
+ * Add an empty line (fluent version).
+ *
+ * @return $this For chaining
+ */
+ public function addEmptyLine() {
+ $this->append('');
+ return $this;
}
/**
* Start building a docblock.
@@ -264,7 +281,6 @@ public function constant(
public function docblock(string $description = '') : DocblockBuilder {
return new DocblockBuilder($this, $description);
}
- /**
/**
* Add an attribute for a class.
*
From 240a415e660c76b30451f588c8a53981f17a8744 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 21 Jan 2026 18:23:03 +0300
Subject: [PATCH 33/88] refactor: Multiple Enhancements
- Remove @Since
- Added `ClassWriter` as return type
- Made `writeClassBody` as the only abstract method
---
WebFiori/Framework/Writers/ClassWriter.php | 95 ++++++++++++++--------
1 file changed, 59 insertions(+), 36 deletions(-)
diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php
index 29b694e32..a70167bcb 100644
--- a/WebFiori/Framework/Writers/ClassWriter.php
+++ b/WebFiori/Framework/Writers/ClassWriter.php
@@ -2,7 +2,7 @@
/**
* This file is licensed under MIT License.
*
- * Copyright (c) 2020 Ibrahim BinAlshikh
+ * Copyright (c) 2020 WebFiori Framework
*
* For more information on the license, please visit:
* https://github.com/WebFiori/.github/blob/main/LICENSE
@@ -17,7 +17,6 @@
*
* @author Ibrahim
*
- * @version 1.0.1
*/
abstract class ClassWriter {
/**
@@ -25,7 +24,6 @@ abstract class ClassWriter {
*
* @var string
*
- * @since 1.0
*/
private $classLines;
/**
@@ -33,7 +31,6 @@ abstract class ClassWriter {
*
* @var string
*
- * @since 1.0
*/
private $className;
/**
@@ -45,11 +42,13 @@ abstract class ClassWriter {
/**
* The location at which the entity class will be created on.
*
- * @since 1.0
*/
private $path;
private $suffix;
private $useArr;
+ private $extendsClass;
+ private $implementsInterfaces;
+ private $classComment;
/**
* Creates new instance of the class.
*
@@ -102,8 +101,6 @@ public function addUseStatement($classesToUse) {
}
/**
* Appends a string or array of strings to the string that represents the
-
- return $this;
* body of the class.
*
* @param string $strOrArr The string that will be appended. At the end of the string
@@ -112,7 +109,6 @@ public function addUseStatement($classesToUse) {
* @param int $tabsCount The number of tabs that will be added to the string.
* A tab is represented as 4 spaces.
*
- * @since 1.0
*/
public function append($strOrArr, $tabsCount = 0) {
if (gettype($strOrArr) != 'array') {
@@ -155,7 +151,7 @@ public function f($funcName, $argsArr = [], ?string $returns = null) {
* @param bool $isAbstract Is abstract method
* @param bool $isFinal Is final method
*
- * @return $this For chaining
+ * @return ClassWriter For chaining
*/
public function method(
string $funcName,
@@ -252,7 +248,7 @@ public function property(
* @param string $value Constant value as string
* @param string $visibility Visibility: 'public', 'protected', 'private'
*
- * @return $this For chaining
+ * @return ClassWriter For chaining
*/
public function constant(
string $name,
@@ -265,9 +261,9 @@ public function constant(
/**
* Add an empty line (fluent version).
*
- * @return $this For chaining
+ * @return ClassWriter For chaining
*/
- public function addEmptyLine() {
+ public function addEmptyLine() : ClassWriter {
$this->append('');
return $this;
}
@@ -288,9 +284,9 @@ public function docblock(string $description = '') : DocblockBuilder {
* @param array $params Attribute parameters
* @param int $indent Indentation level
*
- * @return $this For chaining
+ * @return ClassWriter For chaining
*/
- public function classAttribute(string $name, array $params = [], int $indent = 0) {
+ public function classAttribute(string $name, array $params = [], int $indent = 0) : ClassWriter {
$this->append($this->formatAttribute($name, $params), $indent);
return $this;
}
@@ -301,9 +297,9 @@ public function classAttribute(string $name, array $params = [], int $indent = 0
* @param array $params Attribute parameters
* @param int $indent Indentation level
*
- * @return $this For chaining
+ * @return ClassWriter For chaining
*/
- public function propertyAttribute(string $name, array $params = [], int $indent = 1) {
+ public function propertyAttribute(string $name, array $params = [], int $indent = 1) : ClassWriter {
$this->append($this->formatAttribute($name, $params), $indent);
return $this;
}
@@ -314,9 +310,9 @@ public function propertyAttribute(string $name, array $params = [], int $indent
* @param array $params Attribute parameters
* @param int $indent Indentation level
*
- * @return $this For chaining
+ * @return ClassWriter For chaining
*/
- public function methodAttribute(string $name, array $params = [], int $indent = 1) {
+ public function methodAttribute(string $name, array $params = [], int $indent = 1) : ClassWriter {
$this->append($this->formatAttribute($name, $params), $indent);
return $this;
}
@@ -477,12 +473,12 @@ protected function writeMethodStub(
$this->append($this->method($methodName, $params, $returns), $indent);
$this->append('//TODO: Implement this method.', $indent + 1);
$this->append('}', $indent);
- } /**
+ }
+ /**
* Returns the absolute path of the class that will be created.
*
* @return string The absolute path of the file that holds class information.
*
- * @since 1.0.1
*/
public function getAbsolutePath() : string {
return $this->getPath().DS.$this->getName().'.php';
@@ -499,7 +495,6 @@ public function getAbsolutePath() : string {
* @return string The name of the class that will be created. Default is
* 'NewClass'
*
- * @since 1.0
*/
public function getName(bool $withNs = false) : string {
$retVal = $this->className.$this->getSuffix();
@@ -523,7 +518,6 @@ public function getName(bool $withNs = false) : string {
* @return string The namespace at which the generated class will be added to.
* default is '\' which is the global namespace.
*
- * @since 1.0
*/
public function getNamespace() : string {
return $this->ns;
@@ -534,7 +528,6 @@ public function getNamespace() : string {
* @return string The location at which the class will be created on.
* default is the value of the contstant ROOT_PATH
*
- * @since 1.0
*/
public function getPath() : string {
return $this->path;
@@ -646,10 +639,9 @@ public function removeUseStatement(string $classToRemove) {
*
* @param string $name A string that represents class name.
*
- * @return boolean If the name is successfully set, the method will return true.
- * Other than that, false is returned.
+ * @return ClassWriter
*/
- public function setClassName(string $name) {
+ public function setClassName(string $name) : ClassWriter {
$trimmed = trim($name);
if (!self::isValidClassName($trimmed)) {
@@ -668,10 +660,9 @@ public function setClassName(string $name) {
*
* @param string $namespace
*
- * @return boolean If the namespace is successfully set, the method will return true.
- * Other than that, false is returned.
+ * @return ClassWriter
*/
- public function setNamespace(string $namespace) {
+ public function setNamespace(string $namespace) : ClassWriter {
$trimmed = trim($namespace, ' ');
if (!self::isValidNamespace($trimmed)) {
@@ -689,10 +680,9 @@ public function setNamespace(string $namespace) {
*
* @param string $path A string that represents folder path.
*
- * @return boolean If the path is successfully set, the method will return true.
- * Other than that, false is returned.
+ * @return ClassWriter
*/
- public function setPath(string $path) {
+ public function setPath(string $path) : ClassWriter {
$trimmed = trim($path);
if (strlen($trimmed) == 0) {
@@ -708,9 +698,9 @@ public function setPath(string $path) {
* @param string $classNameSuffix A string to append to class name such as 'Table' or
* 'Service'. It must be a string which is considered as valid class name.
*
- * @return bool If set, the method will return true. False otherises.
+ * @return ClassWriter
*/
- public function setSuffix(string $classNameSuffix) {
+ public function setSuffix(string $classNameSuffix) : ClassWriter {
if (!self::isValidClassName($classNameSuffix)) {
throw new \InvalidArgumentException(
"Invalid suffix '$classNameSuffix'. Suffix must be a valid class name."
@@ -721,13 +711,45 @@ public function setSuffix(string $classNameSuffix) {
$this->className = $this->fixClassName($this->className);
return $this;
}
+ /**
+ * Sets the parent class to extend.
+ *
+ * @param string|null $class The class to extend
+ *
+ * @return ClassWriter For chaining
+ */
+ public function setExtends(?string $class) : ClassWriter {
+ $this->extendsClass = $class;
+ return $this;
+ }
+ /**
+ * Sets the interfaces to implement.
+ *
+ * @param array $interfaces Array of interface names
+ *
+ * @return ClassWriter For chaining
+ */
+ public function setImplements(array $interfaces) : ClassWriter {
+ $this->implementsInterfaces = $interfaces;
+ return $this;
+ }
+ /**
+ * Sets a custom class comment.
+ *
+ * @param string $comment The class comment/description
+ *
+ * @return ClassWriter For chaining
+ */
+ public function setClassComment(string $comment) : ClassWriter {
+ $this->classComment = $comment;
+ return $this;
+ }
/**
* Write the new class to a .php file.
*
* Note that the method will remove the file if it was already created and create
* new one.
*
- * @since 1.0
*/
public function writeClass() {
$classFile = new File($this->getName().'.php', $this->getPath());
@@ -748,7 +770,8 @@ public function getCode() : string {
$this->writeClassDeclaration();
$this->writeClassBody();
return implode("\n", $this->normalizeCode($this->classLines));
- } public abstract function writeClassBody();
+ }
+ public abstract function writeClassBody();
/**
* Writes the top section of the class that contains class comment.
*/
From acaea2d718abe4d6a08cac0a7c34d1e475937f4c Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 21 Jan 2026 19:01:02 +0300
Subject: [PATCH 34/88] refactor: Removal of Create Command
---
.../Framework/Cli/Commands/CreateCommand.php | 183 ------
.../Cli/Helpers/CreateAPITestCase.php | 145 -----
.../Cli/Helpers/CreateAttributeTable.php | 77 ---
.../Cli/Helpers/CreateBackgroundTask.php | 92 ---
.../Cli/Helpers/CreateCLIClassHelper.php | 109 ----
.../Cli/Helpers/CreateClassHelper.php | 281 ---------
.../Cli/Helpers/CreateDBAccessHelper.php | 118 ----
.../Cli/Helpers/CreateDomainEntity.php | 54 --
.../Cli/Helpers/CreateFullRESTHelper.php | 400 -------------
.../Cli/Helpers/CreateMiddleware.php | 73 ---
.../Framework/Cli/Helpers/CreateMigration.php | 118 ----
.../Cli/Helpers/CreateRepository.php | 62 --
.../Cli/Helpers/CreateRestService.php | 100 ----
.../Framework/Cli/Helpers/CreateTableObj.php | 74 ---
.../Cli/Helpers/CreateThemeHelper.php | 35 --
.../Cli/Helpers/CreateWebService.php | 159 -----
.../Framework/Cli/Helpers/TableObjHelper.php | 561 ------------------
.../Tests/Cli/CreateAPITestCaseTest.php | 242 --------
.../Tests/Cli/CreateAttributeTableTest.php | 98 ---
.../Tests/Cli/CreateCLICommandTest.php | 125 ----
.../Framework/Tests/Cli/CreateCommandTest.php | 70 ---
.../Tests/Cli/CreateDBAccessTest.php | 129 ----
.../Tests/Cli/CreateDomainEntityTest.php | 93 ---
.../Framework/Tests/Cli/CreateEntityTest.php | 115 ----
.../Tests/Cli/CreateMiddlewareTest.php | 99 ----
.../Tests/Cli/CreateMigrationTest.php | 174 ------
.../Framework/Tests/Cli/CreateRESTTest.php | 418 -------------
.../Tests/Cli/CreateRepositoryTest.php | 93 ---
.../Framework/Tests/Cli/CreateTableTest.php | 279 ---------
.../Framework/Tests/Cli/CreateTaskTest.php | 229 -------
.../Framework/Tests/Cli/CreateThemeTest.php | 137 -----
.../Tests/Cli/CreateWebServiceTest.php | 336 -----------
32 files changed, 5278 deletions(-)
delete mode 100644 WebFiori/Framework/Cli/Commands/CreateCommand.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateAPITestCase.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateAttributeTable.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateBackgroundTask.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateCLIClassHelper.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateClassHelper.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateDBAccessHelper.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateDomainEntity.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateFullRESTHelper.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateMiddleware.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateMigration.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateRepository.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateRestService.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateTableObj.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateThemeHelper.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/CreateWebService.php
delete mode 100644 WebFiori/Framework/Cli/Helpers/TableObjHelper.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateAPITestCaseTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateAttributeTableTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateCLICommandTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateCommandTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateDBAccessTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateDomainEntityTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateEntityTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateRESTTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateRepositoryTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateTableTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateTaskTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateThemeTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateWebServiceTest.php
diff --git a/WebFiori/Framework/Cli/Commands/CreateCommand.php b/WebFiori/Framework/Cli/Commands/CreateCommand.php
deleted file mode 100644
index 642a5e019..000000000
--- a/WebFiori/Framework/Cli/Commands/CreateCommand.php
+++ /dev/null
@@ -1,183 +0,0 @@
-println('We need from you to give us entity class information.');
- $infoReader = new ClassInfoReader($this);
- $classInfo = $infoReader->readClassInfo($defaultNs);
- $implJsonI = $this->confirm('Would you like from your class to implement the interface JsonI?', true);
-
- $mapper = $tableObj->getEntityMapper();
-
- if ($this->confirm('Would you like to add extra attributes to the entity?', false)) {
- $addExtra = true;
-
- while ($addExtra) {
- if ($mapper->addAttribute($this->getInput('Enter attribute name:'))) {
- $this->success('Attribute successfully added.');
- } else {
- $this->warning('Unable to add attribute.');
- }
- $addExtra = $this->confirm('Would you like to add another attribute?', false);
- }
- }
- $this->println('Generating your entity...');
- $mapper->setPath($classInfo['path']);
- $mapper->setNamespace($classInfo['namespace']);
- $mapper->setEntityName($classInfo['name']);
- $mapper->setUseJsonI($implJsonI);
- $mapper->create();
- $this->success('Entity class created.');
-
- return 0;
- }
- public function exec() : int {
- $answer = $this->getWhat();
-
- if ($answer == 'Quit.') {
- return 0;
- } else if ($answer == 'Database table class.') {
- $create = new CreateTableObj($this);
- $create->readClassInfo();
- } else if ($answer == 'Attribute-based table schema (Clean Architecture).') {
- $create = new CreateAttributeTable($this);
- $create->writeClass();
- } else if ($answer == 'Entity class from table.') {
- $this->createEntityFromQuery();
- } else if ($answer == 'Pure domain entity (Clean Architecture).') {
- $create = new CreateDomainEntity($this);
- $create->writeClass();
- } else if ($answer == 'Web service.') {
- $create = new CreateWebService($this);
- $create->readClassInfo();
- } else if ($answer == 'Annotation-based REST service (Clean Architecture).') {
- $create = new CreateRestService($this);
- $create->writeClass();
- } else if ($answer == 'Middleware.') {
- $create = new CreateMiddleware($this);
- $create->readClassInfo();
- } else if ($answer == 'CLI Command.') {
- $create = new CreateCLIClassHelper($this);
- $create->readClassInfo();
- } else if ($answer == 'Background Task.') {
- $create = new CreateBackgroundTask($this);
- $create->readClassInfo();
- } else if ($answer == 'Theme.') {
- $create = new CreateThemeHelper($this);
- $create->readClassInfo();
- } else if ($answer == 'Database access class based on table.') {
- $create = new CreateDBAccessHelper($this);
- $create->setTable(CLIUtils::readTable($this));
- $this->println('We need from you to give us class information.');
- $create->readDbClassInfo();
- $this->println('We need from you to give us entity class information.');
- $create->readEntityInfo();
- $create->confirnIncludeColsUpdate();
- $create->writeClass();
- } else if ($answer == 'Repository class (Clean Architecture).') {
- $create = new CreateRepository($this);
- $create->writeClass();
- } else if ($answer == 'Complete clean architecture stack (Entity + Table + Repository).') {
- $create = new CreateCleanArchStack($this);
- return $create->writeClasses();
- } else if ($answer == 'Complete REST backend (Database table, entity, database access and web services).') {
- $create = new CreateFullRESTHelper($this);
- $create->readInfo();
- } else if ($answer == 'Web service test case.') {
- $create = new CreateAPITestCase($this);
- if (!$create->readClassInfo()) {
- return -1;
- }
- } else if ($answer == 'Database migration.') {
- $create = new CreateMigration($this);
- $create->writeClass();
- return 0;
- }
-
- return 0;
- }
- private function getWhat() {
- $options = [];
- $options['table'] = 'Database table class.';
- $options['table-attributes'] = 'Attribute-based table schema (Clean Architecture).';
- $options['entity'] = 'Entity class from table.';
- $options['domain-entity'] = 'Pure domain entity (Clean Architecture).';
- $options['web-service'] = 'Web service.';
- $options['rest-service'] = 'Annotation-based REST service (Clean Architecture).';
- $options['task'] = 'Background Task.';
- $options['middleware'] = 'Middleware.';
- $options['command'] = 'CLI Command.';
- $options['theme'] = 'Theme.';
- $options['db'] = 'Database access class based on table.';
- $options['repository'] = 'Repository class (Clean Architecture).';
- $options['clean-stack'] = 'Complete clean architecture stack (Entity + Table + Repository).';
- $options['rest'] = 'Complete REST backend (Database table, entity, database access and web services).';
- $options['api-test'] = 'Web service test case.';
- $options['migration'] = 'Database migration.';
- $options['q'] = 'Quit.';
- $what = $this->getArgValue('--c');
- $answer = null;
-
- if ($what !== null) {
- $answer = $options[strtolower($what)] ?? null;
-
- if ($answer === null) {
- $this->warning('The argument --c has invalid value.');
- }
- }
-
- if ($answer === null) {
- $answer = $this->select('What would you like to create?', $options, count($options) - 1);
- }
-
- return $answer;
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateAPITestCase.php b/WebFiori/Framework/Cli/Helpers/CreateAPITestCase.php
deleted file mode 100644
index 6a8d10742..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateAPITestCase.php
+++ /dev/null
@@ -1,145 +0,0 @@
-writer = $this->getWriter();
- }
- public function readClassInfo() {
- if (!$this->readManagerInfo()) {
- return false;
- }
- if (!$this->readServiceInfo()) {
- return false;
- }
- $nsArr = explode('\\', get_class($this->writer->getService()));
- array_pop($nsArr);
- $ns = implode('\\', $nsArr);
- $this->setClassName($this->writer->getServiceName().'Test');
- $this->setNamespace($ns);
- $this->setPath(ROOT_PATH.DS.'tests'.DS.implode(DS, $nsArr));
-
-
- if ($this->getCommand()->isArgProvided('--defaults')) {
- $this->writeClass();
- } else {
- $this->checkPlace($ns);
- }
-
- return true;
- }
- private function checkPlace($ns) {
- $this->println("Test case will be created with following parameters:");
- $this->println("PHPUnit Version: ".$this->writer->getPhpUnitVersion());
- $this->println("Name: ".$this->getWriter()->getName(true));
- $this->println("Path: ".$this->getWriter()->getPath());
- $confrm = $this->confirm('Would you like to use default parameters?', true);
-
- if ($confrm) {
- $this->writeClass();
- } else {
- $this->writer->setPhpUnitVersion($this->getCommand()->readInteger('PHPUnit Version:', 11));
- $this->setClassInfo($ns, 'Test');
- $this->setPath(ROOT_PATH.DS.'tests'.DS.$this->writer->getNamespace());
- $this->writeClass();
- }
- }
- private function readManagerInfo() : bool {
- $m = $this->getCommand()->getArgValue('--manager');
- $instance = null;
-
- if ($m !== null) {
- try {
- if (class_exists($m)) {
-
- $instance = new $m();
-
-
- if ($instance instanceof WebServicesManager) {
- $this->writer->setServicesManager($instance);
-
- return true;
- } else {
- $this->error("The argument --manager has invalid value: Not an instance of ".WebServicesManager::class);
-
- return false;
- }
-
- } else {
- $this->error("The argument --manager has invalid value: Not a class: ".$m);
-
- return false;
- }
- } catch (\Throwable $ex) {
- $this->error("The argument --manager has invalid value: ".$ex->getMessage());
- return false;
- }
- }
-
- if ($instance === null) {
- while (!($instance instanceof WebServicesManager)) {
- $instance = $this->getCommand()->readInstance('Please enter services manager information:');
-
- if (!($instance instanceof WebServicesManager)) {
- $this->error('Provided class is not an instance of '.WebServicesManager::class);
- } else {
- $this->writer->setServicesManager($instance);
-
- return true;
- }
- }
- }
- return false;
- }
- private function readServiceInfo() : bool {
- $selected = $this->getCommand()->getArgValue('--service');
- $services = $this->writer->getServicesManager()->getServices();
-
- if ($selected !== null) {
- if (!isset($services[$selected])) {
- $this->info('Selected services manager has no service with name \''.$selected.'\'.');
- } else {
- $this->writer->setService($services[$selected]);
-
- return true;
- }
- }
-
- if (count($services) == 0) {
- $this->info('Provided services manager has 0 registered services.');
-
- return false;
- }
- $selected = $this->select('Which service you would like to have a test case for?', array_keys($services));
- $this->writer->setService($services[$selected]);
- return true;
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateAttributeTable.php b/WebFiori/Framework/Cli/Helpers/CreateAttributeTable.php
deleted file mode 100644
index 8dc15a48c..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateAttributeTable.php
+++ /dev/null
@@ -1,77 +0,0 @@
-isArgProvided('--defaults')) {
- $ns = $this->getCommand()->getInput('Table schema namespace: Enter = \''.$ns.'\'') ?: $ns;
- }
-
- $this->setNamespace($ns);
- $this->setClassName($command->readClassName('Enter table class name:', 'Table'));
-
- $tableName = $this->getInput('Enter database table name:');
- $this->getWriter()->setTableName($tableName);
-
- if (!$command->isArgProvided('--defaults')) {
- $this->readColumns();
- }
- }
-
- private function readColumns() {
- $this->println('Add columns to the table:');
-
- while (true) {
- $name = $this->getInput('Column name (or press Enter to finish):');
- if (empty($name)) {
- break;
- }
-
- $type = $this->select('Column type:', [
- 'INT', 'VARCHAR', 'TEXT', 'DATETIME', 'TIMESTAMP', 'BOOL', 'DOUBLE', 'DECIMAL'
- ], 1);
-
- $options = [];
-
- if ($type === 'VARCHAR' || $type === 'DECIMAL') {
- $size = (int)$this->getInput('Size:', $type === 'VARCHAR' ? '255' : '10');
- $options['size'] = $size;
- }
-
- if ($this->confirm('Is primary key?', false)) {
- $options['primary'] = true;
- if ($type === 'INT' && $this->confirm('Auto increment?', true)) {
- $options['autoIncrement'] = true;
- }
- }
-
- if ($this->confirm('Is nullable?', false)) {
- $options['nullable'] = true;
- }
-
- $this->getWriter()->addColumn($name, $type, $options);
- $this->success("Added column: $name");
- }
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateBackgroundTask.php b/WebFiori/Framework/Cli/Helpers/CreateBackgroundTask.php
deleted file mode 100644
index 26d6b1cc3..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateBackgroundTask.php
+++ /dev/null
@@ -1,92 +0,0 @@
-taskWriter = $this->getWriter();
- }
- public function readClassInfo() {
- $this->setClassInfo(APP_DIR.'\\Tasks', 'Task');
- $taskName = $this->getTaskName();
- $taskDesc = $this->getTaskDesc();
-
- if ($this->confirm('Would you like to add arguments to the task?', false)) {
- $this->getArgsHelper();
- }
-
- $this->taskWriter->setTaskName($taskName);
- $this->taskWriter->setTaskDescription($taskDesc);
-
- $this->writeClass();
- }
- private function getArgsHelper() {
- $addToMore = true;
-
- while ($addToMore) {
- try {
- $argObj = new TaskArgument($this->getInput('Enter argument name:'));
- $argObj->setDescription($this->getInput('Describe the use of the argument:', ''));
- $argObj->setDefault($this->getInput('Default value:', ''));
-
- $this->taskWriter->addArgument($argObj);
- } catch (InvalidArgumentException $ex) {
- $this->error($ex->getMessage());
- }
- $addToMore = $this->confirm('Would you like to add more arguments?', false);
- }
- }
- private function getTaskDesc(): string {
- return $this->getInput('Provide short description of what does the task will do:', null, new InputValidator(function ($val)
- {
- if (strlen($val) > 0) {
- return true;
- }
-
- return false;
- }));
- }
- private function getTaskName() : string {
- return $this->getInput('Enter a name for the task:', null, new InputValidator(function ($val)
- {
- $temp = new BaseTask();
-
- if ($temp->setTaskName($val)) {
- return true;
- }
-
- return false;
- }, 'Provided name is invalid!'));
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateCLIClassHelper.php b/WebFiori/Framework/Cli/Helpers/CreateCLIClassHelper.php
deleted file mode 100644
index 7b05ff331..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateCLIClassHelper.php
+++ /dev/null
@@ -1,109 +0,0 @@
-cliWriter = $this->getWriter();
- }
- public function readClassInfo() {
- $this->setClassInfo(APP_DIR.'\\Commands', 'Command');
- $commandName = $this->getCommandName();
- $commandDesc = $this->getInput('Give a short description of the command:');
-
- if ($this->getCommand()->confirm('Would you like to add arguments to the command?', false)) {
- $argsArr = $this->getArgs();
- } else {
- $argsArr = [];
- }
- $this->cliWriter->setCommandName($commandName);
- $this->cliWriter->setCommandDescription($commandDesc);
- $this->cliWriter->setArgs($argsArr);
-
- $this->writeClass();
- }
- private function getArgs() : array {
- $argsArr = [];
- $addToMore = true;
-
- while ($addToMore) {
- $argObj = new \WebFiori\Cli\Argument();
- $argName = $this->getInput('Enter argument name:');
-
- if (!$argObj->setName($argName)) {
- $this->error('Invalid name provided.');
- continue;
- }
- $argObj->setDescription($this->getInput('Describe this argument and how to use it:', ''));
-
- foreach ($this->getFixedValues() as $v) {
- $argObj->addAllowedValue($v);
- }
- $argObj->setIsOptional($this->confirm('Is this argument optional or not?', true));
- $argObj->setDefault($this->getInput('Enter default value:').'');
-
- $argsArr[] = $argObj;
- $addToMore = $this->confirm('Would you like to add more arguments?', false);
- }
-
- return $argsArr;
- }
- private function getCommandName(): string {
- return $this->getInput('Enter a name for the command:', null, new InputValidator(function ($val)
- {
- if (strlen($val) > 0 && strpos($val, ' ') === false) {
- return true;
- }
-
- return false;
- }));
- }
- private function getFixedValues() : array {
- if (!$this->confirm('Does this argument have a fixed set of values?', false)) {
- return [];
- }
- $addValues = true;
- $valuesArr = [];
-
- while ($addValues) {
- $val = $this->getInput('Enter the value:');
-
- if (!in_array($val, $valuesArr)) {
- $valuesArr[] = $val;
- } else {
- $this->info('Given value was already added.');
- }
- $addValues = $this->confirm('Would you like to add more values?', false);
- }
-
- return $valuesArr;
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateClassHelper.php b/WebFiori/Framework/Cli/Helpers/CreateClassHelper.php
deleted file mode 100644
index 46ecbeaed..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateClassHelper.php
+++ /dev/null
@@ -1,281 +0,0 @@
-command = $command;
- $this->classWriter = $writer;
- $this->classInfoReader = new ClassInfoReader($this->command);
- }
- /**
- * Asks the user to conform something.
- *
- * This method will display the question and wait for the user to confirm the
- * action by entering 'y' or 'n' in the terminal. If the user give something
- * other than 'Y' or 'n', it will shows an error and ask him to confirm
- * again. If a default answer is provided, it will appear in upper case in the
- * terminal. For example, if default is set to true, at the end of the prompt,
- * the string that shows the options would be like '(Y/n)'.
- *
- * @param string $confirmTxt The text of the question which will be asked.
- *
- * @param boolean|null $default Default answer to use if empty input is given.
- * It can be true for 'y' and false for 'n'. Default value is null which
- * means no default will be used.
- *
- * @return boolean If the user choose 'y', the method will return true. If
- * he choose 'n', the method will return false.
- */
- public function confirm(string $confirmTxt, ?bool $default = null) {
- return $this->getCommand()->confirm($confirmTxt, $default);
- }
- /**
- * Display a message that represents an error.
- *
- * The message will be prefixed with the string 'Error:' in
- * red.
- *
- * @param string $message The message that will be shown.
- */
- public function error(string $message) {
- $this->getCommand()->error($message);
- }
- /**
- * Initiate the CLI process which is used to read class information.
- *
- * @param string $defaultNs Default namespace to use in case the user did not
- * provide one.
- *
- * @param string $suffix A string to append to the name of the class if it
- * was not in provided name.
- */
- public function getClassInfo(?string $defaultNs = null, ?string $suffix = null) {
- return $this->classInfoReader->readClassInfo($defaultNs, $suffix);
- }
- /**
- * Returns the command which is used to read inputs and show outputs.
- *
- * @return Command
- */
- public function getCommand() : Command {
- return $this->command;
- }
- /**
- * Take an input value from the user.
- *
- * @param string $prompt The string that will be shown to the user. The
- * string must be non-empty.
- *
- * @param string $default An optional default value to use in case the user
- * hit "Enter" without entering any value. If null is passed, no default
- * value will be set.
- *
- * @param InputValidator $validator A validator that can be used to validate user
- * input. If the value is valid, the callback must return true.
- * If the callback returns anything else, it means the value which is given
- * by the user is invalid and this method will ask the user to enter the
- * value again.
- *
- * @param array $validatorParams An optional array that can hold extra parameters
- * which can be passed to the validation callback.
- *
- * @return string The method will return the value which was taken from the
- * user.
- *
- */
- public function getInput(string $prompt, ?string $default = null, ?InputValidator $validator = null) {
- return $this->getCommand()->getInput($prompt, $default, $validator);
- }
- /**
- * Returns an instance of the class that is used to write the final output.
- *
- * @return ClassWriter An instance of the class that is used to write the final output.
- */
- public function getWriter() : ClassWriter {
- return $this->classWriter;
- }
- /**
- * Display a message that represents extra information.
- *
- * The message will be prefixed with the string 'Info:' in
- * blue.
- *
- * @param string $message The message that will be shown.
- *
- */
- public function info(string $message) {
- $this->getCommand()->info($message);
- }
- /**
- * Print out a string and terminates the current line by writing the
- * line separator string.
- *
- * This method will work like the function fprintf(). The difference is that
- * it will print out to the stream at which was specified by the method
- * Command::setOutputStream() and the text can have formatting
- * options. Note that support for output formatting depends on terminal support for
- * ANSI escape codes.
- *
- * @param string $str The string that will be printed.
- *
- * @param mixed $_ One or more extra arguments that can be supplied to the
- * method. The last argument can be an array that contains text formatting options.
- * for available options, check the method Command::formatOutput().
- */
- public function println($str = '', ...$_) {
- $this->getCommand()->println($str, $_);
- }
- /**
- * Ask the user to select one of multiple values.
- *
- * This method will display a prompt and wait for the user to select
- * the a value from a set of values. If the user give something other than the listed values,
- * it will shows an error and ask him to select again again. The
- * user can select an answer by typing its text or its number which will appear
- * in the terminal.
- *
- * @param string $prompt The text that will be shown for the user.
- *
- * @param array $choices An indexed array of values to select from.
- *
- * @param int $defaultIndex The index of the default value in case no value
- * is selected and the user hit enter.
- *
- * @return string The method will return the value which is selected by
- * the user.
- *
- * @since 1.0
- */
- public function select($prompt, $choices, $defaultIndex = -1) {
- return $this->getCommand()->select($prompt, $choices, $defaultIndex);
- }
- /**
- * Initiate the CLI process which is used to read class information.
- *
- * @param string $ns Default namespace to use in case the user did not
- * provide one.
- *
- * @param string $suffix A string to append to the name of the class if it
- * was not in provided name.
- */
- public function setClassInfo(string $ns, string $suffix) {
- $classInfo = $this->getClassInfo($ns, $suffix);
- $this->setNamespace($classInfo['namespace']);
-
- if ($suffix != $classInfo['name']) {
- $this->setClassName($classInfo['name']);
- }
- $this->setPath($classInfo['path']);
- }
- /**
- * Sets the name of the class will be created on.
- *
- * @param string $name A string that represents class name.
- *
- * @return boolean If the name is successfully set, the method will return true.
- * Other than that, false is returned.
- */
- public function setClassName(string $name) : bool {
- return $this->getWriter()->setClassName($name);
- }
- /**
- * Sets the namespace of the class that will be created.
- *
- * @param string $ns The namespace.
- *
- * @return boolean If the namespace is successfully set, the method will return true.
- * Other than that, false is returned.
- */
- public function setNamespace($ns) : bool {
- return $this->getWriter()->setNamespace($ns);
- }
- /**
- * Sets the location at which the class will be created on.
- *
- * @param string $path A string that represents folder path.
- *
- * @return boolean If the path is successfully set, the method will return true.
- * Other than that, false is returned.
- */
- public function setPath(string $path): bool {
- return $this->getWriter()->setPath($path);
- }
- /**
- * Display a message that represents a success status.
- *
- * The message will be prefixed with the string "Success:" in green.
- *
- * @param string $message The message that will be displayed.
- *
- */
- public function success($message) {
- $this->getCommand()->success($message);
- }
- /**
- * Display a message that represents a warning.
- *
- * The message will be prefixed with the string 'Warning:' in
- * red.
- *
- * @param string $message The message that will be shown.
- *
- */
- public function warning(string $message) {
- $this->getCommand()->warning($message);
- }
- /**
- * Creates the class which is based on the writer.
- *
- * @param bool $showOutput If this is set to true, a message which
- * states that a new class was created at the location which was specified
- * by the writer.
- */
- public function writeClass(bool $showOutput = true) {
- $this->getWriter()->writeClass();
-
- if ($showOutput) {
- $this->info('New class was created at "'.$this->getWriter()->getPath().'".');
- }
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateDBAccessHelper.php b/WebFiori/Framework/Cli/Helpers/CreateDBAccessHelper.php
deleted file mode 100644
index a7b592991..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateDBAccessHelper.php
+++ /dev/null
@@ -1,118 +0,0 @@
-getCommand()->confirm('Would you like to have update methods for every single column?', false)) {
- $this->getWriter()->includeColumnsUpdate();
- }
- }
- /**
- * Returns the table at which the database access class will be associated with.
- *
- * @return Table The table at which the database access class will be associated with.
- */
- public function getTable() : Table {
- return $this->getWriter()->getTable();
- }
- /**
- * Prompt the user for basic database class information including name and
- * the namespace at which the class will be added to.
- */
- public function readDbClassInfo() {
- $info = $this->getClassInfo(APP_DIR.'\\Database', 'DB');
- $this->getWriter()->setNamespace($info['namespace']);
- $this->getWriter()->setPath(ROOT_PATH.DS.$info['namespace']);
- $this->getWriter()->setClassName($info['name']);
- $this->getWriter()->setConnection($this->getConnection());
- }
-
- public function readEntityInfo() {
- $t = $this->getTable();
- $m = $t->getEntityMapper();
- $m->setEntityName($this->getCommand()->readClassName('Entity class name:', null));
- $m->setNamespace($this->getCommand()->readNamespace('Entity namespace:', APP_DIR.'\\Entity'));
- }
- public function readTable() {
- $tableClassNameValidity = false;
- $tableClassName = $this->getCommand()->getArgValue('--table');
- $tableObj = null;
-
- do {
- if ($tableClassName === null || strlen($tableClassName) == 0) {
- $tableClassName = $this->getCommand()->getInput('Enter database table class name (include namespace):');
- }
-
- if (!class_exists($tableClassName)) {
- $this->getCommand()->error('Class not found.');
- $tableClassName = '';
- continue;
- }
- $tableObj = new $tableClassName();
-
- if (!$tableObj instanceof Table) {
- $this->getCommand()->error('The given class is not a child of the class "WebFiori\Database\Table".');
- $tableClassName = '';
- continue;
- }
- $tableClassNameValidity = true;
- } while (!$tableClassNameValidity);
-
- $this->setTable($tableObj);
- }
- /**
- * Sets the table at which the database access class will be associated with.
- *
- * @param Table $t The table at which the database access class will be associated with.
- */
- public function setTable(Table $t) {
- $this->getWriter()->setTable($t);
- }
- private function getConnection() {
- $dbConnections = array_keys(App::getConfig()->getDBConnections());
-
- if (count($dbConnections) != 0) {
- $dbConnections[] = 'None';
- $conn = $this->select('Select database connecion to use with the class:', $dbConnections, count($dbConnections) - 1);
-
- if ($conn != 'None') {
- return $conn;
- }
- } else {
- $this->warning('No database connections were found. Make sure to specify connection later inside the class.');
- }
-
- return '';
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateDomainEntity.php b/WebFiori/Framework/Cli/Helpers/CreateDomainEntity.php
deleted file mode 100644
index cb351138c..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateDomainEntity.php
+++ /dev/null
@@ -1,54 +0,0 @@
-isArgProvided('--defaults')) {
- $ns = $this->getCommand()->getInput('Entity namespace: Enter = \''.$ns.'\'') ?: $ns;
- }
-
- $this->setNamespace($ns);
- $this->setClassName($command->readClassName('Enter entity class name:'));
-
- if (!$command->isArgProvided('--defaults')) {
- $this->readProperties();
- }
- }
-
- private function readProperties() {
- $this->println('Add properties to the entity:');
-
- while (true) {
- $name = $this->getInput('Property name (or press Enter to finish):');
- if (empty($name)) {
- break;
- }
-
- $type = $this->select('Property type:', ['int', 'string', 'bool', 'float', 'array'], 1);
- $nullable = $this->confirm('Is nullable?', false);
-
- $this->getWriter()->addProperty($name, $type, $nullable);
- $this->success("Added property: $name");
- }
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateFullRESTHelper.php b/WebFiori/Framework/Cli/Helpers/CreateFullRESTHelper.php
deleted file mode 100644
index a15324ece..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateFullRESTHelper.php
+++ /dev/null
@@ -1,400 +0,0 @@
-tableObjWriter->getEntityName();
- }
- /**
- *
- * @return Table|null
- */
- public function getTable() {
- return $this->tableObjWriter->getTable();
- }
- public function readInfo() {
- $connection = CLIUtils::getConnectionName($this->getCommand());
-
- if ($connection === null) {
- $dbType = $this->select('Database type:', ConnectionInfo::SUPPORTED_DATABASES);
- } else {
- $dbType = $connection->getDatabaseType();
- }
-
- $tempTable = new MySQLTable();
-
- if ($dbType == 'mssql') {
- $tempTable = new MSSQLTable();
- }
- $this->tableObjWriter = new TableClassWriter($tempTable);
- $this->readEntityInfo();
-
- $entityName = $this->tableObjWriter->getEntityName();
- $this->tableObjWriter->setClassName($entityName.'Table');
- $this->readTableInfo();
-
- $t = $this->tableObjWriter->getTable();
- $t->getEntityMapper()->setEntityName($this->tableObjWriter->getEntityName());
- $t->getEntityMapper()->setNamespace($this->tableObjWriter->getEntityNamespace());
- $t->getEntityMapper()->setPath($this->tableObjWriter->getEntityPath());
- $this->dbObjWriter = new DBClassWriter($this->tableObjWriter->getEntityName().'DB', $this->tableObjWriter->getNamespace(), $t);
-
- if ($connection !== null) {
- $this->dbObjWriter->setConnection($connection->getName());
- }
-
- if ($this->confirm('Would you like to have update methods for every single column?', false)) {
- $this->dbObjWriter->includeColumnsUpdate();
- }
- $this->readAPIInfo();
- $this->createEntity();
- $this->createTableClass();
- $this->createDbClass();
- $this->writeServices();
- $this->println("Done.");
- }
- private function addDeleteGetProcessCode(WebServiceWriter $w, $uniqueParamsArr, $type) {
- $dbClassName = $this->dbObjWriter->getName();
- $entityName = $this->dbObjWriter->getEntityName();
- $paramsStrArr = [];
-
- foreach ($uniqueParamsArr as $p) {
- $paramsStrArr[] = "\$this->getParamVal('".$p['name']."')";
- }
- $paramsStr = implode(", ", $paramsStrArr);
-
- if ($type == 'GetSingle') {
- $w->addProcessCode([
- '$entity = '.$dbClassName.'::get()->get'.$entityName.'('.$paramsStr.');',
- "\$this->send('application/json', new Json(["
- ]);
- $w->addProcessCode([
- "'data' => \$entity"
- ], 3);
- $w->addProcessCode([
- ']));'
- ]);
- } else if ($type == 'Delete') {
- $w->addProcessCode([
- '$entity = '.$dbClassName.'::get()->get'.$entityName.'('.$paramsStr.');',
- $dbClassName.'::get()->delete'.$entityName.'($entity);',
- "\$this->sendResponse('Record Removed.');"
- ]);
- }
- }
- private function addSingleUpdateCode() {
- }
- private function createDbClass() {
- $this->println("Creating database access class...");
- $this->dbObjWriter->writeClass();
- }
- private function createEntity() {
- $this->println("Creating entity class...");
- $this->tableObjWriter->getTable()->getEntityMapper()->create();
- }
-
- private function createTableClass() {
- $this->println("Creating database table class...");
- $this->tableObjWriter->writeClass();
- }
- private function getAPIParamType($colDatatype): string {
- if ($colDatatype == 'int') {
- return 'int';
- }
-
- if ($colDatatype == 'bool' || $colDatatype == 'boolean') {
- return 'boolean';
- }
-
- if ($colDatatype == 'decimal' || $colDatatype == 'money') {
- return 'double';
- }
-
- return 'string';
- }
- private function getServiceSuffix($entityName): string {
- $suffix = '';
-
- for ($x = 0 ; $x < strlen($entityName) ; $x++) {
- $ch = $entityName[$x];
-
- if ($x != 0 && $ch >= 'A' && $ch <= 'Z') {
- $suffix .= '-'.strtolower($ch);
- continue;
- }
- $suffix .= strtolower($ch);
- }
-
- return $suffix;
- }
- private function getUniqueAPIParams() : array {
- $params = [];
- $t = $this->getTable();
-
- foreach ($t->getColsKeys() as $paramName) {
- $colObj = $t->getColByKey($paramName);
-
- if ($colObj->isUnique()) {
- $paramArr = [
- 'name' => $paramName,
- 'type' => $this->getAPIParamType($colObj->getDatatype()),
- ];
-
- if ($colObj->getDefault() !== null) {
- $paramArr['default'] = $colObj->getDefault();
- }
-
- if ($colObj->isNull()) {
- $paramArr['optional'] = true;
- }
- $params[] = $paramArr;
- }
- }
-
- return $params;
- }
- private function IncludeAPISetProps(WebServiceWriter $w, $type) {
- $t = $this->getTable();
- $w->addProcessCode('$entity = $this->getObject('.$t->getEntityMapper()->getEntityName().'::class);');
-
-
- $dbClassName = $this->dbObjWriter->getName();
- $entityName = $this->dbObjWriter->getEntityName();
- $w->addProcessCode("");
-
- if ($type == 'Add') {
- $w->addProcessCode("$dbClassName::get()->add$entityName(\$entity);");
- $w->addProcessCode("\$this->sendResponse('Record Created.');");
- } else if ($type == 'Update') {
- $w->addProcessCode("$dbClassName::get()->update$entityName(\$entity);");
- $w->addProcessCode("\$this->sendResponse('Record Updated.');");
- }
- }
-
- private function readAPIInfo() {
- $this->apisNs = CLIUtils::readNamespace($this->getCommand(), APP_DIR.'\\Apis',"Last thing needed is to provide us with namespace for web services:");
- }
- private function readEntityInfo() {
- $this->println("First thing, we need entity class information.");
- $entityInfo = $this->getClassInfo(APP_DIR.'\\Entity');
- $entityInfo['implement-jsoni'] = $this->confirm('Would you like from your entity class to implement the interface JsonI?', true);
- $this->tableObjWriter->setEntityInfo($entityInfo['name'], $entityInfo['namespace'], $entityInfo['path'], $entityInfo['implement-jsoni']);
-
- if ($this->confirm('Would you like to add extra attributes to the entity?', false)) {
- $addExtra = true;
-
- while ($addExtra) {
- if ($this->tableObjWriter->getTable()->getEntityMapper()->addAttribute($this->getInput('Enter attribute name:'))) {
- $this->success('Attribute added.');
- } else {
- $this->warning('Unable to add attribute.');
- }
- $addExtra = $this->confirm('Would you like to add another attribute?', false);
- }
- }
- }
- private function readTableInfo() {
- $this->println("Now, time to collect database table information.");
- $ns = CLIUtils::readNamespace($this->getCommand(), APP_DIR.'\\Database', 'Provide us with a namespace for table class:');
- $this->tableObjWriter->setNamespace($ns);
- $this->tableObjWriter->setPath(ROOT_PATH.DS.$ns);
-
- $create = new CreateTableObj($this->getCommand());
- $create->getWriter()->setTable($this->tableObjWriter->getTable());
- $tableHelper = new TableObjHelper($create, $this->tableObjWriter->getTable());
- $tableHelper->setTableName();
- $tableHelper->setTableComment();
- $tableHelper->getCreateHelper()->setNamespace($ns);
- $tableHelper->getCreateHelper()->setPath(ROOT_PATH.DS.$ns);
- $tableHelper->getCreateHelper()->setClassName($this->tableObjWriter->getName());
- $this->println('Now you have to add columns to the table.');
- $tableHelper->addColumns();
-
- if ($this->confirm('Would you like to add foreign keys to the table?', false)) {
- $tableHelper->addForeignKeys();
- }
- }
-
- private function writeServices() {
- $this->println("Writing web services...");
- $entityName = $this->getEntityName();
- $suffix = $this->getServiceSuffix($entityName);
- $uniqueParams = $this->getUniqueAPIParams();
- $t = $this->getTable();
-
- $servicesPrefix = [
- 'Add'.$entityName => [
- 'type' => 'Add',
- 'name' => 'add-'.$suffix,
- 'method' => 'post',
- ],
- 'Update'.$entityName => [
- 'type' => 'Update',
- 'name' => 'update-'.$suffix,
- 'method' => 'post'
- ],
- 'Delete'.$entityName => [
- 'type' => 'Delete',
- 'name' => 'delete-'.$suffix,
- 'method' => 'delete'
- ],
- 'Get'.$entityName => [
- 'type' => 'GetSingle',
- 'name' => 'get-'.$suffix,
- 'method' => 'get'
- ],
- 'GetAll'.$entityName.'s' => [
- 'type' => 'GetAll',
- 'name' => 'get-all-'.$suffix.'s',
- 'method' => 'get'
- ]
- ];
- $w = $this->dbObjWriter;
-
- if ($w->isColumnUpdateIncluded()) {
- $uniqueCols = $this->tableObjWriter->getTable()->getUniqueColsKeys();
- $colsKeys = $this->tableObjWriter->getTable()->getColsKeys();
-
- foreach ($colsKeys as $colKey) {
- if (!in_array($colKey, $uniqueCols)) {
- $colObj = $t->getColByKey($colKey);
- $paramProps = [
- 'name' => $colKey,
- 'type' => $this->getAPIParamType($colObj->getDatatype())
- ];
- $idxName = 'Update'.DBClassWriter::toMethodName($colKey, '').'Of'.$entityName;
- $servicesPrefix[$idxName] = [
- 'name' => 'update-'.$colKey.'-of-'.$suffix,
- 'method' => 'post',
- 'type' => 'SingleUpdate',
- 'params' => array_merge([$paramProps], $uniqueParams)
- ];
- }
- }
- }
-
- foreach ($servicesPrefix as $sName => $serviceProps) {
- $service = new ServiceHolder($serviceProps['name']);
- $service->addRequestMethod($serviceProps['method']);
- $t = $this->getTable();
-
-
- $writer = new WebServiceWriter($service);
- $writer->addUseStatement($this->dbObjWriter->getName(true));
- $writer->addUseStatement($t->getEntityMapper()->getEntityName(true));
- $writer->addUseStatement(Json::class);
- $writer->setNamespace($this->apisNs);
- $writer->setPath(ROOT_PATH.DS.$this->apisNs);
- $writer->setClassName($sName);
- $apiType = $serviceProps['type'];
-
- if ($apiType == 'Add' || $apiType == 'Update') {
- $this->IncludeAPISetProps($writer, $apiType);
-
- foreach ($t->getColsKeys() as $paramName) {
- $colObj = $t->getColByKey($paramName);
- $paramArr = [
- 'name' => $paramName,
- 'type' => $this->getAPIParamType($colObj->getDatatype()),
- ];
-
- if ($colObj->getDefault() !== null) {
- $paramArr['default'] = $colObj->getDefault();
- }
-
- if ($colObj->isNull()) {
- $paramArr['optional'] = true;
- }
- $service->addParameter($paramArr);
- }
- } else if ($apiType == 'SingleUpdate') {
- foreach ($serviceProps['params'] as $p) {
- $service->addParameter($p);
- }
- $this->addSingleUpdateCode();
- } else if ($apiType == 'GetSingle' || $apiType == 'Delete') {
- if (count($uniqueParams) != 0) {
- foreach ($uniqueParams as $p) {
- $service->addParameter($p);
- }
- $this->addDeleteGetProcessCode($writer, $uniqueParams, $apiType);
- }
- } else if ($apiType == 'GetAll') {
- $service->addParameters([
- 'page' => [
- 'type' => 'int',
- 'default' => 1
- ],
- 'size' => [
- 'type' => 'int',
- 'default' => 10
- ]
- ]);
- $dbClassName = $this->dbObjWriter->getName();
- $entityName = $this->dbObjWriter->getEntityName();
- $writer->addProcessCode([
- "\$pageNumber = \$this->getParamVal('page');",
- "\$pageSize = \$this->getParamVal('size');",
- '$recordsCount = '.$dbClassName.'::get()->get'.$entityName.'sCount();',
- '$data = '.$dbClassName.'::get()->get'.$entityName.'s($pageNumber, $pageSize);',
- "\$this->send('application/json', new Json(["
- ]);
- $writer->addProcessCode([
- "'page' => new Json(["
- ], 3);
-
- $writer->addProcessCode([
- "'pages-count' => ceil(\$recordsCount/\$pageSize),",
- "'size' => \$pageSize,",
- "'page-number' => \$pageNumber,",
- ], 4);
-
- $writer->addProcessCode([
- "]),"
- ], 3);
- $writer->addProcessCode([
- "'data' => \$data"
- ], 3);
- $writer->addProcessCode([
- ']));'
- ]);
- }
- $writer->writeClass();
- }
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateMiddleware.php b/WebFiori/Framework/Cli/Helpers/CreateMiddleware.php
deleted file mode 100644
index 32b7fbe35..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateMiddleware.php
+++ /dev/null
@@ -1,73 +0,0 @@
-mdWriter = $this->getWriter();
- }
- public function readClassInfo() {
- $this->setClassInfo(APP_DIR.'\\Middleware', 'Middleware');
-
- $middlewareName = $this->getMiddlewareName();
- $priority = $this->getCommand()->readInteger('Enter middleware priority:', 0);
-
- if ($this->confirm('Would you like to add the middleware to a group?', false)) {
- $this->getGroups();
- }
-
- $this->mdWriter->setMiddlewareName($middlewareName);
- $this->mdWriter->setMiddlewarePriority($priority);
- $this->writeClass();
- }
- private function getGroups() {
- $addToMore = true;
-
- while ($addToMore) {
- $groupName = $this->getInput('Enter group name:');
-
- if (strlen($groupName) > 0) {
- $this->mdWriter->addGroup($groupName);
- }
- $addToMore = $this->confirm('Would you like to add the middleware to another group?', false);
- }
- }
- private function getMiddlewareName() : string {
- return $this->getInput('Enter a name for the middleware:', null, new InputValidator(function ($val)
- {
- if (strlen(trim($val)) > 0) {
- return true;
- }
-
- return false;
- }));
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateMigration.php b/WebFiori/Framework/Cli/Helpers/CreateMigration.php
deleted file mode 100644
index 6efe737fb..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateMigration.php
+++ /dev/null
@@ -1,118 +0,0 @@
-command = $command;
- $this->generator = new DatabaseChangeGenerator();
-
- $ns = APP_DIR.'\\Database\\Migrations';
- if (!$command->isArgProvided('--defaults')) {
- $ns = CLIUtils::readNamespace($command, $ns , 'Migration namespace:');
- }
-
- $this->generator->setNamespace($ns);
- $this->generator->setPath(APP_PATH.'Database'.DS.'Migrations');
-
- $this->className = $command->readClassName('Provide a name for the class that will have migration logic:', null);
-
- if (!$command->isArgProvided('--defaults')) {
- $this->readDependencies();
- }
- }
-
- public function writeClass() {
- $options = [];
-
- if (!empty($this->dependencies)) {
- $options[GeneratorOption::DEPENDENCIES] = $this->dependencies;
- }
-
- $filePath = $this->generator->createMigration($this->className, $options);
- $this->command->info('New class was created at "'.dirname($filePath).'".');
- }
-
- private function readDependencies() {
- if (!$this->command->confirm('Does this migration depend on other migrations?', false)) {
- return;
- }
-
- $migrations = $this->getExistingMigrations();
-
- if (empty($migrations)) {
- $this->command->warning('No existing migrations found.');
- return;
- }
-
- $this->command->println('Available migrations:');
- foreach ($migrations as $idx => $migration) {
- $this->command->println("$idx: $migration");
- }
-
- while (true) {
- $input = $this->command->getInput('Enter migration number (or press Enter to finish):');
-
- if (empty($input)) {
- break;
- }
-
- $idx = (int)$input;
- if (isset($migrations[$idx])) {
- $fullClass = '\\'.$this->generator->getNamespace().'\\'.$migrations[$idx];
- $this->dependencies[] = $fullClass;
- $this->command->success("Added dependency: {$migrations[$idx]}");
- } else {
- $this->command->error('Invalid migration number.');
- }
- }
- }
-
- private function getExistingMigrations() : array {
- $migrationsDir = APP_PATH.'Database'.DS.'Migrations';
-
- if (!is_dir($migrationsDir)) {
- return [];
- }
-
- $files = scandir($migrationsDir);
- $migrations = [];
-
- foreach ($files as $file) {
- if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
- $migrations[] = pathinfo($file, PATHINFO_FILENAME);
- }
- }
-
- return $migrations;
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateRepository.php b/WebFiori/Framework/Cli/Helpers/CreateRepository.php
deleted file mode 100644
index 850550158..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateRepository.php
+++ /dev/null
@@ -1,62 +0,0 @@
-isArgProvided('--defaults')) {
- $ns = $this->getCommand()->getInput('Repository namespace: Enter = \''.$ns.'\'') ?: $ns;
- }
-
- $this->setNamespace($ns);
- $this->setClassName($command->readClassName('Enter repository class name:', 'Repository'));
-
- $entityClass = $this->getInput('Enter entity class (e.g., App\\Domain\\User):');
- $this->getWriter()->setEntityClass($entityClass);
-
- $tableName = $this->getInput('Enter table name:');
- $this->getWriter()->setTableName($tableName);
-
- $idField = $this->getInput('Enter ID field name:', 'id');
- $this->getWriter()->setIdField($idField);
-
- if (!$command->isArgProvided('--defaults')) {
- $this->readProperties();
- }
- }
-
- private function readProperties() {
- $this->println('Add entity properties (for mapping):');
-
- while (true) {
- $name = $this->getInput('Property name (or press Enter to finish):');
- if (empty($name)) {
- break;
- }
-
- $type = $this->select('Property type:', ['int', 'string', 'bool', 'float'], 1);
-
- $this->getWriter()->addProperty($name, $type);
- $this->success("Added property: $name");
- }
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateRestService.php b/WebFiori/Framework/Cli/Helpers/CreateRestService.php
deleted file mode 100644
index 572cb2524..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateRestService.php
+++ /dev/null
@@ -1,100 +0,0 @@
-isArgProvided('--defaults')) {
- $ns = $this->getCommand()->getInput('Service namespace: Enter = \''.$ns.'\'') ?: $ns;
- }
-
- $this->setNamespace($ns);
- $this->setClassName($command->readClassName('Enter service class name:', 'Service'));
-
- $description = $this->getInput('Service description:');
- $this->getWriter()->setDescription($description);
-
- if (!$command->isArgProvided('--defaults')) {
- $this->readMethods();
- }
- }
-
- private function readMethods() {
- $this->println('Add HTTP methods to the service:');
-
- while (true) {
- $httpMethod = $this->select('HTTP method (or select Cancel):', ['GET', 'POST', 'PUT', 'DELETE', 'Cancel'], 0);
-
- if ($httpMethod === 'Cancel') {
- break;
- }
-
- $methodName = $this->getInput('Method name:');
- $params = [];
-
- if ($this->confirm('Add parameters?', false)) {
- $params = $this->readParameters();
- }
-
- $returnType = $this->select('Return type:', ['array', 'string', 'int', 'bool'], 0);
-
- $this->getWriter()->addMethod($httpMethod, $methodName, $params, $returnType);
- $this->success("Added method: $methodName");
-
- if (!$this->confirm('Add another method?', false)) {
- break;
- }
- }
- }
-
- private function readParameters(): array {
- $params = [];
-
- while (true) {
- $name = $this->getInput('Parameter name (or press Enter to finish):');
- if (empty($name)) {
- break;
- }
-
- $type = $this->select('Parameter type:', ['STRING', 'INT', 'DOUBLE', 'BOOL', 'EMAIL', 'URL', 'ARRAY'], 0);
- $description = $this->getInput('Parameter description:');
-
- $param = [
- 'name' => $name,
- 'type' => $type,
- 'description' => $description
- ];
-
- if ($type === 'INT' || $type === 'DOUBLE') {
- if ($this->confirm('Set min/max values?', false)) {
- $param['min'] = (int)$this->getInput('Minimum value:');
- $param['max'] = (int)$this->getInput('Maximum value:');
- }
- }
-
- $params[] = $param;
- $this->success("Added parameter: $name");
- }
-
- return $params;
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateTableObj.php b/WebFiori/Framework/Cli/Helpers/CreateTableObj.php
deleted file mode 100644
index 2d0111eee..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateTableObj.php
+++ /dev/null
@@ -1,74 +0,0 @@
-select('Database type:', ConnectionInfo::SUPPORTED_DATABASES);
-
- if ($databaseType == 'mysql') {
- $tempTable = new MySQLTable();
- } else if ($databaseType == 'mssql') {
- $tempTable = new MSSQLTable();
- }
- $this->getWriter()->setTable($tempTable);
- $this->setClassInfo(APP_DIR.'\\Database', 'Table');
-
- $tableHelper = new TableObjHelper($this, $tempTable);
- $tableHelper->setTableName(CaseConverter::toSnakeCase($this->getWriter()->getName()));
- $tableHelper->setTableComment();
-
- $this->println('Now you have to add columns to the table.');
- $tableHelper->addColumns();
-
-
-
- if ($this->confirm('Would you like to add foreign keys to the table?', false)) {
- $tableHelper->addForeignKeys();
- }
-
- $withEntity = false;
-
- if ($this->confirm('Would you like to create an entity class that maps to the database table?', false)) {
- $tableHelper->createEntity();
- $withEntity = true;
- }
-
- $this->writeClass();
-
- if ($withEntity) {
- $this->info('Entity class was created at "'.$this->getWriter()->getEntityPath().'".');
- }
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateThemeHelper.php b/WebFiori/Framework/Cli/Helpers/CreateThemeHelper.php
deleted file mode 100644
index 5a487d7cf..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateThemeHelper.php
+++ /dev/null
@@ -1,35 +0,0 @@
-setClassInfo('Themes', 'Theme');
-
- $this->println('Creating theme at "'.$this->getWriter()->getPath().'"...');
- $this->writeClass();
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/CreateWebService.php b/WebFiori/Framework/Cli/Helpers/CreateWebService.php
deleted file mode 100644
index 6792c0e72..000000000
--- a/WebFiori/Framework/Cli/Helpers/CreateWebService.php
+++ /dev/null
@@ -1,159 +0,0 @@
-serviceObj = new ServiceHolder();
- parent::__construct($command, new WebServiceWriter($this->serviceObj));
- }
- public function addRequestMethods() {
- $toSelect = RequestMethod::getAll();
- $addOne = true;
-
- while ($addOne) {
- array_multisort($toSelect);
- $this->serviceObj->addRequestMethod($this->select('Request method:', $toSelect, 2));
-
- if (count($toSelect) > 1) {
- $addOne = $this->confirm('Would you like to add another request method?', false);
- } else {
- $addOne = false;
- }
- }
- }
- public function readClassInfo() {
- $this->setClassInfo(APP_DIR.'\\Apis', 'Service');
-
- $this->setServiceName();
- $this->serviceObj->setDescription($this->getInput('Description:'));
- $this->addRequestMethods();
-
- if ($this->confirm('Would you like to add request parameters to the service?', false)) {
- $this->addParamsToService();
- }
-
- $this->println('Creating the class...');
- $this->writeClass();
- $this->info('Don\'t forget to add the service to a services manager.');
- }
- private function addParamsToService() {
- do {
- $paramObj = new RequestParameter('h');
- $this->setParamName($paramObj);
- $paramObj->setType($this->select('Choose parameter type:', ParamType::getTypes(), 0));
- $paramObj->setDescription($this->getInput('Description:'));
- $added = $this->serviceObj->addParameter($paramObj);
- $paramObj->setIsOptional($this->confirm('Is this parameter optional?', true));
-
- if ($paramObj->getType() == ParamType::STRING || $paramObj->getType() == ParamType::URL || $paramObj->getType() == ParamType::EMAIL) {
- $paramObj->setIsEmptyStringAllowed($this->confirm('Are empty values allowed?', false));
- $this->setMinAndMaxLength($paramObj);
- }
-
- if ($paramObj->getType() == ParamType::INT || $paramObj->getType() == ParamType::DOUBLE) {
- $this->setMinAndMax($paramObj);
- }
-
- if ($added) {
- $this->success('New parameter added.');
- } else {
- $this->warning('The parameter was not added.');
- }
- $addMore = $this->confirm('Would you like to add another parameter?', false);
- } while ($addMore);
- }
- private function setMinAndMax(RequestParameter $param) {
- $setMinMax = $this->confirm('Would you like to set minimum and maximum limites?', false);
-
- if (!$setMinMax) {
- return;
- }
- $isValid = false;
- $method = $param->getType() == ParamType::INT ? 'readInteger' : 'readFloat';
-
- while (!$isValid) {
- $min = $this->getCommand()->$method('Minimum value:');
- $max = $this->getCommand()->$method('Maximum value:');
-
- if ($min < $max) {
- $param->setMinValue($min);
- $param->setMaxValue($max);
- $isValid = true;
- } else {
- $this->error('Minimum and maximum should not overlap.');
- }
- }
- }
- private function setMinAndMaxLength(RequestParameter $param) {
- $setMinMax = $this->confirm('Would you like to set minimum and maximum length?', false);
-
- if (!$setMinMax) {
- return;
- }
- $isValid = false;
-
- while (!$isValid) {
- $min = $this->getCommand()->readInteger('Minimum length:');
- $max = $this->getCommand()->readInteger('Maximum length:');
-
- if ($min < $max) {
- $param->setMinLength($min);
- $param->setMaxLength($max);
- $isValid = true;
- } else {
- $this->error('Minimum and maximum should not overlap.');
- }
- }
- }
- /**
- *
- * @param RequestParameter $paramObj
- */
- private function setParamName(RequestParameter $paramObj) {
- do {
- $paramName = $this->getInput('Enter a name for the request parameter:');
- $validName = $paramObj->setName($paramName);
-
- if (!$validName) {
- $this->error('Given name is invalid.');
- }
- } while (!$validName);
- }
- private function setServiceName() {
- do {
- $serviceName = $this->getInput('Enter a name for the new web service:');
- $validName = $this->serviceObj->setName($serviceName);
-
- if (!$validName) {
- $this->error('Given name is invalid.');
- }
- } while (!$validName);
- }
-}
diff --git a/WebFiori/Framework/Cli/Helpers/TableObjHelper.php b/WebFiori/Framework/Cli/Helpers/TableObjHelper.php
deleted file mode 100644
index fbff68dc6..000000000
--- a/WebFiori/Framework/Cli/Helpers/TableObjHelper.php
+++ /dev/null
@@ -1,561 +0,0 @@
-createHelper = $c;
- $this->table = $t;
- }
- /**
- * Returns the objects at which the class is using to perform modifications.
- *
- * @return Table
- */
- public function &getTable() : Table {
- return $this->table;
- }
- /**
- * Adds new column to associated table.
- *
- * The method will prompt the user to specify all information of the column
- * including its name, data type, size and so on.
- */
- public function addColumn() {
- $helper = $this->getCreateHelper();
- $tempTable = $this->getTable();
- $colKey = $helper->getInput('Enter a name for column key:');
-
- if ($tempTable->hasColumnWithKey($colKey)) {
- $helper->warning("The table already has a key with name '$colKey'.");
-
- return;
- }
- $col = new MSSQLColumn();
-
- if ($tempTable instanceof MySQLTable) {
- $col = new MySQLColumn();
- }
- $col->setName(str_replace('-', '_', str_replace(' ', '_', $colKey)));
- $colDatatype = $helper->select('Column data type:', $col->getSupportedTypes(), 0);
- $col->setDatatype($colDatatype);
- $isAdded = $tempTable->addColumn($colKey, $col);
-
- if (!$isAdded) {
- $helper->warning('The column was not added. Mostly, key name is invalid. Try again.');
- } else {
- $colObj = $tempTable->getColByKey($colKey);
- $this->setSize($colObj);
- $this->isIdentityCheck($colObj);
- $this->isPrimaryCheck($colObj);
- $this->addColComment($colObj);
-
- if ($helper->getCommand() instanceof UpdateTableCommand) {
- $this->copyCheck();
- } else {
- $this->getCreateHelper()->writeClass(false);
- }
- $helper->success('Column added.');
- }
- }
- /**
- * Prompt the user to add multiple columns.
- *
- * First, the method will add one column, after that, it will ask if extra
- * column should be added or not. If yes, it will ask for new column information.
- * If not, the loop will stop.
- */
- public function addColumns() {
- do {
- $this->addColumn();
- } while ($this->getCreateHelper()->confirm('Would you like to add another column?', false));
- }
- public function addForeignKey() {
- $refTable = null;
- $helper = $this->getCreateHelper();
-
- $refTableName = $helper->getInput('Enter the name of the referenced table class (with namespace):');
- try {
- $refTable = new $refTableName();
- } catch (Throwable $ex) {
- $helper->error($ex->getMessage());
-
- return;
- }
-
- if (!($refTable instanceof Table)) {
- $helper->error('The given class is not an instance of the class \''.Table::class.'\'.');
-
- return;
- }
- $fkName = $helper->getInput('Enter a name for the foreign key:', null, new InputValidator(function ($val)
- {
- $trimmed = trim($val);
-
- if (strlen($trimmed) == 0) {
- return false;
- }
-
- return true;
- }));
- $fkCols = $this->getFkCols();
- $fkColsArr = [];
-
- foreach ($fkCols as $colKey) {
- $fkColsArr[$colKey] = $helper->select('Select the column that will be referenced by the column \''.$colKey.'\':', $refTable->getColsKeys());
- }
- $onUpdate = $helper->select('Choose on update condition:', [
- 'cascade', 'restrict', 'set null', 'set default', 'no action'
- ], 1);
- $onDelete = $helper->select('Choose on delete condition:', [
- 'cascade', 'restrict', 'set null', 'set default', 'no action'
- ], 1);
-
- try {
- $this->getTable()->addReference($refTable, $fkColsArr, $fkName, $onUpdate, $onDelete);
-
- if ($helper->getCommand() instanceof UpdateTableCommand) {
- $this->copyCheck();
- } else {
- $helper->getWriter()->writeClass(false);
- }
- $helper->success('Foreign key added.');
- } catch (Throwable $ex) {
- $helper->error($ex->getMessage());
- }
- }
- public function addForeignKeys() {
- do {
- $this->addForeignKey();
- } while ($this->getCreateHelper()->confirm('Would you like to add another foreign key?', false));
- }
- /**
- *
- * @return DB|null
- */
- public function confirmRunQuery() {
- $runQuery = $this->confirm('Would you like to update the database?', false);
-
- if ($runQuery) {
- $dbConnections = array_keys(App::getConfig()->getDBConnections());
-
- if (count($dbConnections) == 0) {
- $this->error('No database connections available. Add connections inside the class \'AppConfig\' or use the command "add".');
-
- return null;
- }
- $dbConn = $this->select('Select database connection:', $dbConnections, 0);
-
- return new DB($dbConn);
- }
- }
- public function copyCheck() {
- $helper = $this->getCreateHelper();
-
- if ($helper->confirm('Would you like to update same class or create a copy with the update?', false)) {
- $info = $helper->getClassInfo(APP_DIR.'\\Database', 'Table');
- $helper->setClassName($info['name']);
- $helper->setNamespace($info['namespace']);
- $helper->setPath($info['path']);
- $helper->getWriter()->writeClass(false);
- } else {
- $helper->getWriter()->writeClass(false);
- }
- }
- /**
- * Creates entity class based on associated table object.
- */
- public function createEntity() {
- $helper = $this->getCreateHelper();
- $entityInfo = $helper->getClassInfo(APP_DIR.'\\Entity');
- $entityInfo['implement-jsoni'] = $helper->confirm('Would you like from your entity class to implement the interface JsonI?', true);
- $helper->getWriter()->setEntityInfo($entityInfo['name'], $entityInfo['namespace'], $entityInfo['path'], $entityInfo['implement-jsoni']);
-
- if ($helper->confirm('Would you like to add extra attributes to the entity?', false)) {
- $addExtra = true;
-
- while ($addExtra) {
- if ($this->getTable()->getEntityMapper()->addAttribute($this->getInput('Enter attribute name:'))) {
- $helper->success('Attribute added.');
- } else {
- $helper->warning('Unable to add attribute.');
- }
- $addExtra = $helper->confirm('Would you like to add another attribute?', false);
- }
- }
- }
- /**
- *
- * @return Column
- */
- public function dropColumn() {
- $colsKeys = $this->getTable()->getColsKeys();
-
- if (count($colsKeys) == 0) {
- $this->info('The table has no columns. Nothing to drop.');
-
- return;
- }
- $colToDrop = $this->getCreateHelper()->select('Which column would you like to drop?', $colsKeys);
- $this->getTable()->removeColByKey($colToDrop);
- $class = get_class($this->getTable());
- $this->setClassInfo($class);
- $this->copyCheck();
- $this->getCreateHelper()->success('Column dropped.');
-
- return $colToDrop;
- }
- /**
- *
- * @return CreateTableObj
- */
- public function getCreateHelper() : CreateTableObj {
- return $this->createHelper;
- }
- /**
- * Extract and return the name of table class based on associated table object.
- *
- * @return string The name of table class based on associated table object.
- */
- public function getTableClassName() :string {
- $clazz = get_class($this->getTable());
- $split = explode('\\', $clazz);
-
- if (count($split) > 1) {
- return $split[count($split) - 1];
- }
-
- return $split[0];
- }
- /**
- * Removes a foreign key from associated table object.
- *
- * The method will simply ask the user which key he would like to remove.
- *
- */
- public function removeFk() {
- $tableObj = $this->getTable();
- $helper = $this->getCreateHelper();
-
- if ($tableObj->getForeignKeysCount() == 0) {
- $helper->info('Selected table has no foreign keys.');
-
- return;
- }
- $fks = $tableObj->getForeignKeys();
- $optionsArr = [];
-
- foreach ($fks as $fkObj) {
- $optionsArr[] = $fkObj->getKeyName();
- }
- $toRemove = $helper->select('Select the key that you would like to remove:', $optionsArr);
- $tableObj->removeReference($toRemove);
-
- $this->getCreateHelper()->writeClass(false);
- $helper->success('Table updated.');
- }
- public function removeForeignKey() {
- $tableObj = $this->getTable();
- $helper = $this->getCreateHelper();
-
- if ($tableObj->getForeignKeysCount() == 0) {
- $helper->info('Selected table has no foreign keys.');
-
- return;
- }
- $fks = $tableObj->getForeignKeys();
- $optionsArr = [];
-
- foreach ($fks as $fkObj) {
- $optionsArr[] = $fkObj->getKeyName();
- }
- $toRemove = $helper->select('Select the key that you would like to remove:', $optionsArr);
- $tableObj->removeReference($toRemove);
-
- $this->setClassInfo(get_class($tableObj));
-
- $this->copyCheck();
- $helper->success('Table updated.');
- }
- /**
- * Sets a comment for associated table.
- *
- * The method will prompt the user for optional comment.
- * If empty string provided, the comment will not be set.
- */
- public function setTableComment() {
- $helper = $this->getCreateHelper();
- $tableComment = $helper->getInput('Enter your optional comment about the table:');
-
- if (strlen($tableComment) != 0) {
- $this->getTable()->setComment($tableComment);
- }
- }
- /**
- * Sets the name of the table in the database.
- *
- * The method will prompt the user to set the name of the table as it will
- * appear in the database. This name may not be same as class name
- * of the table.
- *
- * @param string $defaultName A string to set as default table name in case
- * of hitting 'enter' without providing a value.
- */
- public function setTableName(?string $defaultName = null) {
- $invalidTableName = true;
- $helper = $this->getCreateHelper();
-
- do {
- $tableName = $helper->getInput('Enter database table name:', $defaultName);
- $invalidTableName = !$this->getTable()->setName($tableName);
-
- if ($invalidTableName) {
- $helper->error('The given name is invalid.');
- }
- } while ($invalidTableName);
- }
- public function updateColumn() {
- $tableObj = $this->getTable();
- $colsKeys = $tableObj->getColsKeys();
- $helper = $this->getCreateHelper();
-
- if (count($colsKeys) == 0) {
- $helper->info('The table has no columns. Nothing to update.');
-
- return;
- }
- $colToUpdate = $helper->select('Which column would you like to update?', $colsKeys);
- $col = $tableObj->removeColByKey($colToUpdate);
-
- $colKey = $helper->getInput('Enter a new name for column key:', $colToUpdate);
- $isAdded = $tableObj->addColumn($colKey, $col);
-
- if ($colKey != $colToUpdate) {
- $col->setName(str_replace('-', '_', $colKey));
- } else {
- $isAdded = true;
- }
-
- if (!$isAdded) {
- $helper->warning('The column was not added. Mostly, key name is invalid.');
- } else {
- $colDatatype = $helper->select('Select column data type:', $col->getSupportedTypes(), 0);
- $col->setDatatype($colDatatype);
- $this->setSize($col);
- $this->isPrimaryCheck($col);
- $this->addColComment($col);
- }
-
- $this->setClassInfo(get_class($tableObj));
- $this->copyCheck();
- $helper->success('Column updated.');
- }
- /**
- * Prompt the user to set an optional comment for table column.
- *
- * @param Column $colObj The object that the comment will be associated with.
- */
- private function addColComment(Column $colObj) {
- $comment = $this->getCreateHelper()->getInput('Enter your optional comment about the column:');
-
- if (strlen($comment) != 0) {
- $colObj->setComment($comment);
- }
- }
- private function getFkCols() {
- $colNumber = 1;
- $keys = $this->getTable()->getColsKeys();
- $fkCols = [];
- $helper = $this->getCreateHelper();
-
- do {
- $colKey = $helper->select('Select column #'.$colNumber.':', $keys);
-
- if (in_array($colKey, $fkCols)) {
- $helper->error('The column is already added.');
- continue;
- }
- $fkCols[] = $colKey;
- $colNumber++;
- } while ($helper->confirm('Would you like to add another column to the foreign key?', false));
-
- return $fkCols;
- }
- /**
- *
- * @param MSSQLColumn $colObj
- */
- private function isIdentityCheck($colObj) {
- if ($colObj instanceof MSSQLColumn) {
- $dataType = $colObj->getDatatype();
- $t = $this->getTable();
-
- if (($dataType == 'int' || $dataType == 'bigint') && !$t->hasIdentity()) {
- $colObj->setIsIdentity($this->getCreateHelper()->confirm('Is this column an identity column?', false));
- }
- }
- }
- /**
- *
- * @param Column $colObj
- */
- private function isPrimaryCheck(Column $colObj) {
- $helper = $this->getCreateHelper();
- $colObj->setIsPrimary($helper->confirm('Is this column primary?', false));
- $type = $colObj->getDatatype();
-
- if (!$colObj->isPrimary()) {
- if (!($type == 'bool' || $type == 'boolean')) {
- $colObj->setIsUnique($helper->confirm('Is this column unique?', false));
- }
- $this->setDefaultValue($colObj);
- $colObj->setIsNull($helper->confirm('Can this column have null values?', false));
- } else if ($colObj->getDatatype() == 'int' && $colObj instanceof MySQLColumn) {
- $colObj->setIsAutoInc($helper->confirm('Is this column auto increment?', false));
- }
- }
- private function setClassInfo($class) {
- $createHelper = $this->getCreateHelper();
- $split = explode('\\', $class);
- $cName = $split[count($split) - 1];
- $ns = implode('\\', array_slice($split, 0, count($split) - 1));
-
- $path = ROOT_PATH.DS.$ns.DS.$cName.'.php';
- $createHelper->setClassName($cName);
- $createHelper->setNamespace($ns);
- $createHelper->setPath(substr($path, 0, strlen($path) - strlen($cName.'.php')));
- }
- /**
- *
- * @param Column $colObj
- */
- private function setDefaultValue(Column $colObj) {
- $helper = $this->getCreateHelper();
-
- if (!($colObj->getDatatype() == 'bool' || $colObj->getDatatype() == 'boolean')) {
- $defaultVal = $helper->getInput('Enter default value (Hit "Enter" to skip):', '');
-
- if (strlen($defaultVal) != 0) {
- $colObj->setDefault($defaultVal);
- }
-
- return;
- }
- $defaultVal = $helper->getInput('Enter default value (true or false) (Hit "Enter" to skip):', '');
-
- if ($defaultVal == 'true') {
- $colObj->setDefault(true);
- } else if ($defaultVal == 'false') {
- $colObj->setDefault(false);
- }
- }
- /**
- *
- * @param Column $colObj
- */
- private function setScale(Column $colObj) {
- $colDataType = $colObj->getDatatype();
-
- if ($colDataType == 'decimal' || $colDataType == 'float' || $colDataType == 'double') {
- $validScale = false;
-
- do {
- $scale = $this->getCreateHelper()->getInput('Enter the scale (number of numbers to the right of decimal point):');
- $validScale = $colObj->setScale($scale);
-
- if (!$validScale) {
- $this->getCreateHelper()->error('Invalid scale value.');
- }
- } while (!$validScale);
- }
- }
- /**
- *
- * @param Column $colObj
- */
- private function setSize(Column $colObj) {
- $type = $colObj->getDatatype();
- $helper = $this->getCreateHelper();
- $mySqlSupportSize = $type == 'int'
- || $type == 'varchar'
- || $type == 'decimal'
- || $type == 'float'
- || $type == 'double'
- || $type == 'text';
- $mssqlSupportSize = $type == 'char'
- || $type == 'nchar'
- || $type == 'varchar'
- || $type == 'nvarchar'
- || $type == 'binary'
- || $type == 'varbinary'
- || $type == 'decimal'
- || $type == 'float';
-
- if (($colObj instanceof MySQLColumn && $mySqlSupportSize)
- || $colObj instanceof MSSQLColumn && $mssqlSupportSize) {
- $valid = false;
-
- do {
- $colDataType = $colObj->getDatatype();
- $dataSize = $helper->getCommand()->readInteger('Enter column size:');
-
- if ($colObj instanceof MySQLColumn && $colObj->getDatatype() == 'varchar' && $dataSize > 21845) {
- $helper->warning('The data type "varchar" has a maximum size of 21845. The '
- .'data type of the column will be changed to "mediumtext" if you continue.');
-
- if (!$helper->confirm('Would you like to change data type?', false)) {
- $valid = true;
- continue;
- }
- }
-
- if ($colDataType == 'int' && $dataSize > 11) {
- $helper->warning('Size is set to 11 since this is the maximum size for "int" type.');
- }
- $valid = $colObj->setSize($dataSize);
-
- if ($valid) {
- $this->setScale($colObj);
- continue;
- }
- $helper->error('Invalid size is given.');
- } while (!$valid);
- }
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateAPITestCaseTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateAPITestCaseTest.php
deleted file mode 100644
index cae0f8241..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateAPITestCaseTest.php
+++ /dev/null
@@ -1,242 +0,0 @@
-executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--manager' => 'A',
- '--service' => 'c'
- ]);
-
- $this->assertStringContainsString("Error: The argument --manager has invalid value", $output[0]);
- $this->assertEquals(-1, $this->getExitCode());
- }
- /**
- * @test
- */
- public function testCreateAPITestCase01() {
- $path = ROOT_PATH.DS.'tests'.DS."WebFiori".DS."Framework".DS."Scheduler".DS."WebServices";
- $this->assertEquals([
- "Info: Selected services manager has no service with name 'c'.\n",
- "Which service you would like to have a test case for?\n",
- "0: login\n",
- "1: force-execution\n",
- "2: logout\n",
- "3: get-tasks\n",
- "4: set-password\n",
- "Test case will be created with following parameters:\n",
- "PHPUnit Version: 9\n",
- 'Name: WebFiori\\Framework\Scheduler\WebServices\\TasksLoginServiceTest'."\n",
- "Path: ".$path."\n",
- "Would you like to use default parameters?(Y/n)\n",
- "Info: New class was created at \"".$path."\".\n"
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--manager' => TasksServicesManager::class,
- '--service' => 'c'
- ], [
- "0",
- "y"
- ]));
-
- $this->assertEquals(0, $this->getExitCode());
- $clazz = '\\WebFiori\\Framework\Scheduler\WebServices\\TasksLoginServiceTest';
- $this->assertTrue(file_exists($path.DS.'TasksLoginServiceTest.php'));
- require_once $path.DS.'TasksLoginServiceTest.php';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('tests\\WebFiori\\Framework\\Scheduler\\WebServices\\TasksLoginServiceTest');
- }
- /**
- * @test
- */
- public function testCreateAPITestCase02() {
- $path = ROOT_PATH.DS."tests".DS."WebFiori".DS."Framework".DS."Scheduler".DS."WebServices";
- $this->assertEquals([
- "Info: New class was created at \"".$path."\".\n"
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--manager' => TasksServicesManager::class,
- '--service' => 'get-tasks',
- '--defaults'
- ]));
- $this->assertEquals(0, $this->getExitCode());
- $clazz = '\\WebFiori\\Framework\Scheduler\WebServices\\GetTasksServiceTest';
- $this->assertTrue(file_exists($path.DS.'GetTasksServiceTest.php'));
- require_once $path.DS.'GetTasksServiceTest.php';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('tests\\WebFiori\\Framework\\Scheduler\\WebServices\\GetTasksServiceTest');
- }
- /**
- * @test
- */
- public function testCreateAPITestCase03() {
- $path = ROOT_PATH.DS."tests".DS."WebFiori".DS."Framework".DS."Scheduler".DS."WebServices";
- $name = 'GetTasksServiceTest';
- $this->assertEquals([
- "Please enter services manager information:\n",
- "Test case will be created with following parameters:\n",
- "PHPUnit Version: 9\n",
- 'Name: WebFiori\\Framework\Scheduler\WebServices\\'.$name."\n",
- "Path: ".$path."\n",
- "Would you like to use default parameters?(Y/n)\n",
- "PHPUnit Version: Enter = '11'\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'WebFiori\\Framework\Scheduler\WebServices'\n",
- "Info: New class was created at \"".$path."\".\n"
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--service' => 'get-tasks',
- ], [
- '\WebFiori\\Framework\Scheduler\WebServices\\TasksServicesManager',
- 'n',
- '10',
- '',
- '',
- ]));
- $this->assertEquals(0, $this->getExitCode());
-
- $clazz = '\\WebFiori\\Framework\Scheduler\WebServices\\GetTasksServiceTest';
- $this->assertTrue(file_exists($path.DS.'GetTasksServiceTest.php'));
- require_once $path.DS.'GetTasksServiceTest.php';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('tests\\WebFiori\\Framework\\Scheduler\\WebServices\\GetTasksServiceTest');
- }
- /**
- * @test
- */
- public function testCreateAPITestCase04() {
- $path = ROOT_PATH.DS."tests".DS."Apis".DS."Multiple";
- $name = 'WebService00Test';
- $this->assertEquals([
- "Please enter services manager information:\n",
- "Test case will be created with following parameters:\n",
- "PHPUnit Version: 9\n",
- 'Name: Apis\Multiple\\'.$name."\n",
- "Path: ".$path."\n",
- "Would you like to use default parameters?(Y/n)\n",
- "PHPUnit Version: Enter = '11'\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'Apis\Multiple'\n",
- "Info: New class was created at \"".$path."\".\n"
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--service' => 'say-hi-service',
- ], [
- '\\Apis\\Multiple\\ServicesManager00',
- 'n',
- '10',
- '',
- '',
- ]));
- $this->assertEquals(0, $this->getExitCode());
-
- $clazz = '\\Apis\\Multiple\\'.$name;
- $this->assertTrue(file_exists($path.DS.$name.'.php'));
- require_once $path.DS.$name.'.php';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('tests\\Apis\\Multiple\\'.$name);
- }
-
- /**
- * @test
- */
- public function testCreateAPITestCase05() {
- $this->assertEquals([
- "Info: Provided services manager has 0 registered services.\n",
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--manager' => '\\Apis\\EmptyService\\EmptyServicesManager',
- ]));
- $this->assertEquals(-1, $this->getExitCode());
- }
- /**
- * @test
- */
- public function testCreateAPITestCase06() {
- $path = ROOT_PATH.DS."tests".DS."Apis".DS."Multiple";
- $name = 'WebService00Test';
- $this->assertEquals([
- "Please enter services manager information:\n",
- "Error: Provided class is not an instance of ".WebServicesManager::class."\n",
- "Please enter services manager information:\n",
- "Test case will be created with following parameters:\n",
- "PHPUnit Version: 9\n",
- 'Name: Apis\Multiple\\'.$name."\n",
- "Path: ".$path."\n",
- "Would you like to use default parameters?(Y/n)\n",
- "PHPUnit Version: Enter = '11'\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'Apis\Multiple'\n",
- "Info: New class was created at \"".$path."\".\n"
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--service' => 'say-hi-service',
- ], [
- '\\Apis\\Multiple\\WebService00',
- '\\Apis\\Multiple\\ServicesManager00',
- 'n',
- '10',
- '',
- '',
- ]));
- $this->assertEquals(0, $this->getExitCode());
- $clazz = '\\Apis\\Multiple\\'.$name;
- $this->assertTrue(file_exists($path.DS.'WebService00Test.php'));
- require_once $path.DS.$name.'.php';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('tests\\Apis\\Multiple\\'.$name);
- }
- /**
- * @test
- */
- public function testCreateAPITestCase07() {
- $this->assertEquals([
- "Error: The argument --manager has invalid value: Not a class: \\tests\\Apis\\EmptyService\\Xyz\n",
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--manager' => '\\tests\\Apis\\EmptyService\\Xyz',
- ]));
- $this->assertEquals(-1, $this->getExitCode());
- }
- /**
- * @test
- */
- public function testCreateAPITestCase08() {
- $path = ROOT_PATH.DS."tests".DS."Apis".DS."Multiple";
- $this->assertEquals([
- "Info: New class was created at \"".$path."\".\n"
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'api-test',
- '--service' => 'say-hi-service-2',
- '--manager' => '\\Apis\\Multiple\\ServicesManager00',
- '--defaults'
- ]));
- $this->assertEquals(0, $this->getExitCode());
- $name = 'WebService01Test';
- $clazz = '\\Apis\\Multiple\\'.$name;
- $this->assertTrue(file_exists($path.DS.$name.'.php'));
- require_once $path.DS.$name.'.php';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('tests\\Apis\\Multiple\\'.$name);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateAttributeTableTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateAttributeTableTest.php
deleted file mode 100644
index 8debc52cf..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateAttributeTableTest.php
+++ /dev/null
@@ -1,98 +0,0 @@
-cleanupInfrastructure();
- parent::tearDown();
- }
-
- private function cleanupInfrastructure(): void {
- $dir = APP_PATH . 'Infrastructure' . DS . 'Schema';
- if (is_dir($dir)) {
- foreach (glob($dir . DS . '*.php') as $file) {
- unlink($file);
- }
- }
- }
-
- /**
- * @test
- */
- public function testCreateAttributeTable() {
- $output = $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'table-attributes'
- ], [
- "\n", // namespace (use default)
- 'UsersTable', // class name
- 'users', // table name
- 'id', // column name
- '0', // type: INT
- '11', // size
- 'y', // is primary
- 'y', // auto increment
- 'n', // not nullable
- 'name', // column name
- '1', // type: VARCHAR
- '100', // size
- 'n', // not primary
- 'n', // not nullable
- 'email', // column name
- '1', // type: VARCHAR
- '150', // size
- 'n', // not primary
- 'n', // not nullable
- "\n" // finish
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Infrastructure' . DS . 'Schema' . DS . 'UsersTable.php';
- $this->assertTrue(file_exists($filePath), 'Table schema file should exist');
-
- $content = file_get_contents($filePath);
- $this->assertStringContainsString('namespace App\\Infrastructure\\Schema', $content);
- $this->assertStringContainsString('use WebFiori\\Database\\Attributes\\Table', $content);
- $this->assertStringContainsString('use WebFiori\\Database\\Attributes\\Column', $content);
- $this->assertStringContainsString('#[Table(name: \'users\')]', $content);
- $this->assertStringContainsString('#[Column(name: \'id\'', $content);
- $this->assertStringContainsString('primary: true', $content);
- $this->assertStringContainsString('autoIncrement: true', $content);
- $this->assertStringContainsString('#[Column(name: \'name\'', $content);
- $this->assertStringContainsString('#[Column(name: \'email\'', $content);
- $this->assertStringContainsString('class UsersTable', $content);
- }
-
- /**
- * @test
- */
- public function testCreateAttributeTableWithDefaults() {
- $output = $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'table-attributes',
- '--defaults' => ''
- ], [
- 'ProductsTable',
- 'products'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Infrastructure' . DS . 'Schema' . DS . 'ProductsTable.php';
- $this->assertTrue(file_exists($filePath));
-
- $content = file_get_contents($filePath);
- $this->assertStringContainsString('class ProductsTable', $content);
- $this->assertStringContainsString('#[Table(name: \'products\')]', $content);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateCLICommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateCLICommandTest.php
deleted file mode 100644
index 8257947b3..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateCLICommandTest.php
+++ /dev/null
@@ -1,125 +0,0 @@
-executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '5',
- 'NewCLI',
- 'App\Commands',
- 'print-hello',
- 'Prints \'Hello World\' in the console.',
- 'N',
- "\n", // Hit Enter to pick default value
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Commands'\n",
- "Enter a name for the command:\n",
- "Give a short description of the command:\n",
- "Would you like to add arguments to the command?(y/N)\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Commands\".\n",
- ], $output);
- $this->assertTrue(class_exists('\\App\\Commands\\NewCLICommand'));
- $this->removeClass('\\App\\Commands\\NewCLICommand');
- }
-
- /**
- * @test
- */
- public function testCreateCommand01() {
- $clazz = '\\App\\Commands\\DoItCommand';
- if (class_exists($clazz)) {
- $this->removeClass($clazz);
- }
-
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'command'
- ], [
- 'DoIt',
- 'App\Commands',
- 'do-it',
- 'Do something amazing.',
- 'y',
- '--what-to-do',
- "The thing that the command will do.",
- "y",
- "Say Hi",
- "y",
- "Say No",
- "y",
- "Say No", // Duplicate value to test validation
- 'n',
- 'y',
- "\n", // Hit Enter to pick default value (empty default)
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Commands'\n",
- "Enter a name for the command:\n",
- "Give a short description of the command:\n",
- "Would you like to add arguments to the command?(y/N)\n",
- "Enter argument name:\n",
- "Describe this argument and how to use it: Enter = ''\n",
- "Does this argument have a fixed set of values?(y/N)\n",
- "Enter the value:\n",
- "Would you like to add more values?(y/N)\n",
- "Enter the value:\n",
- "Would you like to add more values?(y/N)\n",
- "Enter the value:\n",
- "Info: Given value was already added.\n",
- "Would you like to add more values?(y/N)\n",
- "Is this argument optional or not?(Y/n)\n",
- "Enter default value:\n",
- "Would you like to add more arguments?(y/N)\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Commands\".\n",
- ], $output);
-
- $this->assertTrue(class_exists($clazz));
- $clazzObj = new $clazz();
- $this->assertTrue($clazzObj instanceof Command);
- $this->assertEquals('do-it', $clazzObj->getName());
- $arg = $clazzObj->getArg('--what-to-do');
- $this->assertNotNull($arg);
- $this->assertEquals([
- 'Say Hi', 'Say No'
- ], $arg->getAllowedValues());
- $this->assertTrue($arg->isOptional());
- $this->assertEquals('The thing that the command will do.', $arg->getDescription());
- $this->assertEquals('', $arg->getDefault());
- $this->removeClass($clazz);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateCommandTest.php
deleted file mode 100644
index eacb03f84..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateCommandTest.php
+++ /dev/null
@@ -1,70 +0,0 @@
-executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '11',
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- ], $output);
- }
-
- /**
- * @test
- */
- public function testCreate01() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- "\n", // Hit Enter to pick default value (quit)
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- ], $output);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateDBAccessTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateDBAccessTest.php
deleted file mode 100644
index 65a4d38e2..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateDBAccessTest.php
+++ /dev/null
@@ -1,129 +0,0 @@
-removeAllDBConnections();
-
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'db'
- ], [
- 'Tables\\EmployeeInfoTable',
- 'EmployeeOperations',
- "\n", // Hit Enter to pick default value (App\Database)
- 'SuperUser',
- "\n", // Hit Enter to pick default value (App\Entity)
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Enter database table class name (include namespace):\n",
- "We need from you to give us class information.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Database'\n",
- "Warning: No database connections were found. Make sure to specify connection later inside the class.\n",
- "We need from you to give us entity class information.\n",
- "Entity class name:\n",
- "Entity namespace: Enter = 'App\\Entity'\n",
- "Would you like to have update methods for every single column?(y/N)\n",
- "Info: New class was created at \"". ROOT_PATH.DS."App".DS."Database\".\n"
- ], $output);
- $clazz = '\\App\\Database\\EmployeeOperationsDB';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass($clazz);
- }
-
- /**
- * @test
- */
- public function test01() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'db'
- ], [
- 'Tables\\EmployeeInfoTable',
- 'EmployeeS',
- "\n",
- 'SuperHero',
- "\n",
- 'y'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Enter database table class name (include namespace):\n",
- "We need from you to give us class information.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Database'\n",
- "Warning: No database connections were found. Make sure to specify connection later inside the class.\n",
- "We need from you to give us entity class information.\n",
- "Entity class name:\n",
- "Entity namespace: Enter = 'App\\Entity'\n",
- "Would you like to have update methods for every single column?(y/N)\n",
- "Info: New class was created at \"". ROOT_PATH.DS."App".DS."Database\".\n"
- ], $output);
- $clazz = '\\App\\Database\\EmployeeSDB';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass($clazz);
- }
-
- /**
- * @test
- */
- public function test02() {
- $conn = new ConnectionInfo('mysql', 'root', '123456', 'testing_db', '127.0.0.1', 3306);
- $conn->setName('Test Connection');
- App::getConfig()->removeAllDBConnections();
- App::getConfig()->addOrUpdateDBConnection($conn);
-
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'db'
- ], [
- 'Tables\\PositionInfoTable',
- 'Position2x',
- 'App\\Database',
- '0',
- 'SuperPosition',
- 'App\\Entity\\subs',
- 'y'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Enter database table class name (include namespace):\n",
- "We need from you to give us class information.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Database'\n",
- "Select database connecion to use with the class:\n",
- "0: Test Connection\n",
- "1: None <--\n",
- "We need from you to give us entity class information.\n",
- "Entity class name:\n",
- "Entity namespace: Enter = 'App\\Entity'\n",
- "Would you like to have update methods for every single column?(y/N)\n",
- "Info: New class was created at \"". ROOT_PATH.DS."App".DS."Database\".\n"
- ], $output);
- $clazz = '\\App\\Database\\Position2xDB';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass($clazz);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateDomainEntityTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateDomainEntityTest.php
deleted file mode 100644
index d66a3ae41..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateDomainEntityTest.php
+++ /dev/null
@@ -1,93 +0,0 @@
-cleanupDomain();
- parent::tearDown();
- }
-
- private function cleanupDomain(): void {
- $dir = APP_PATH . 'Domain';
- if (is_dir($dir)) {
- foreach (glob($dir . DS . '*.php') as $file) {
- unlink($file);
- }
- }
- }
-
- /**
- * @test
- */
- public function testCreateDomainEntity() {
- $output = $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'domain-entity'
- ], [
- "\n", // namespace (use default)
- 'User', // class name
- 'id', // property name
- '0', // type: int
- 'y', // nullable
- 'name', // property name
- '1', // type: string
- 'n', // not nullable
- 'email', // property name
- '1', // type: string
- 'n', // not nullable
- "\n" // finish
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Domain' . DS . 'User.php';
- $this->assertTrue(file_exists($filePath), 'Entity file should exist');
-
- $content = file_get_contents($filePath);
- $this->assertStringContainsString('namespace App\\Domain', $content);
- $this->assertStringContainsString('class User', $content);
- $this->assertStringContainsString('public ?int $id', $content);
- $this->assertStringContainsString('public string $name', $content);
- $this->assertStringContainsString('public string $email', $content);
-
- // Test that class can be loaded
- require_once $filePath;
- $this->assertTrue(class_exists('\\App\\Domain\\User'));
-
- // Test instantiation
- $user = new \App\Domain\User(1, 'John Doe', 'john@example.com');
- $this->assertEquals(1, $user->id);
- $this->assertEquals('John Doe', $user->name);
- $this->assertEquals('john@example.com', $user->email);
- }
-
- /**
- * @test
- */
- public function testCreateDomainEntityWithDefaults() {
- $output = $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'domain-entity',
- '--defaults' => ''
- ], [
- 'Product'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Domain' . DS . 'Product.php';
- $this->assertTrue(file_exists($filePath));
-
- $content = file_get_contents($filePath);
- $this->assertStringContainsString('class Product', $content);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateEntityTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateEntityTest.php
deleted file mode 100644
index 625099f03..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateEntityTest.php
+++ /dev/null
@@ -1,115 +0,0 @@
-executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'Entity',
- '--table' => TestTable::class
- ], [
- 'NeEntity',
- "\n", // Hit Enter to pick default value (App\Entity)
- 'y',
- 'y',
- 'superNewAttr',
- 'y',
- 'superNewAttr', // Duplicate attribute name
- 'y',
- 'invalid name', // Invalid attribute name
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "We need from you to give us entity class information.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Entity'\n",
- "Would you like from your class to implement the interface JsonI?(Y/n)\n",
- "Would you like to add extra attributes to the entity?(y/N)\n",
- "Enter attribute name:\n",
- "Success: Attribute successfully added.\n",
- "Would you like to add another attribute?(y/N)\n",
- "Enter attribute name:\n",
- "Warning: Unable to add attribute.\n",
- "Would you like to add another attribute?(y/N)\n",
- "Enter attribute name:\n",
- "Warning: Unable to add attribute.\n",
-
- "Would you like to add another attribute?(y/N)\n",
- "Generating your entity...\n",
- "Success: Entity class created.\n"
- ], $output);
- $this->assertTrue(class_exists('\\App\\Entity\\NeEntity'));
- $this->removeClass('\\App\\Entity\\NeEntity');
- }
-
- /**
- * @test
- */
- public function testCreateEntity01() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'entiy', // Invalid command value
- '--table' => TestTable::class
- ], [
- '1',
- 'NewEntity',
- ' ', // Invalid namespace (spaces only)
- 'y',
- 'y',
- 'superNewAttr',
- 'y',
- 'superNewAttr', // Duplicate attribute name
- 'y',
- 'invalid name', // Invalid attribute name
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Warning: The argument --c has invalid value.\n",
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "We need from you to give us entity class information.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Entity'\n",
- "Would you like from your class to implement the interface JsonI?(Y/n)\n",
- "Would you like to add extra attributes to the entity?(y/N)\n",
- "Enter attribute name:\n",
- "Success: Attribute successfully added.\n",
- "Would you like to add another attribute?(y/N)\n",
- "Enter attribute name:\n",
- "Warning: Unable to add attribute.\n",
- "Would you like to add another attribute?(y/N)\n",
- "Enter attribute name:\n",
- "Warning: Unable to add attribute.\n",
-
- "Would you like to add another attribute?(y/N)\n",
- "Generating your entity...\n",
- "Success: Entity class created.\n"
- ], $output);
- $this->assertTrue(class_exists('\\App\\Entity\\NewEntity'));
- $this->removeClass('\\App\\Entity\\NewEntity');
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareTest.php
deleted file mode 100644
index cde392827..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareTest.php
+++ /dev/null
@@ -1,99 +0,0 @@
-executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '4',
- 'NewCoolMd',
- 'App\Middleware',
- 'Check is authorized',
- '22',
- "\n", // Hit Enter to pick default value (no group)
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Middleware'\n",
- "Enter a name for the middleware:\n",
- "Enter middleware priority: Enter = '0'\n",
- "Would you like to add the middleware to a group?(y/N)\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Middleware\".\n",
- ], $output);
- $this->assertTrue(class_exists('\\App\\Middleware\\NewCoolMdMiddleware'));
- $this->removeClass('\\App\\Middleware\\NewCoolMdMiddleware');
- }
-
- /**
- * @test
- */
- public function testCreateMiddleware01() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'Middleware'
- ], [
- 'NewCool',
- 'App\Middleware',
- ' ', // Invalid input (spaces only)
- 'Check is cool',
- '22',
- 'y',
- 'global',
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Middleware'\n",
- "Enter a name for the middleware:\n",
- "Error: Invalid input is given. Try again.\n",
- "Enter a name for the middleware:\n",
- "Enter middleware priority: Enter = '0'\n",
- "Would you like to add the middleware to a group?(y/N)\n",
- "Enter group name:\n",
- "Would you like to add the middleware to another group?(y/N)\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Middleware\".\n",
- ], $output);
-
- $clazz = '\\App\\Middleware\\NewCoolMiddleware';
- $this->assertTrue(class_exists($clazz));
- $clazzObj = new $clazz();
- $this->assertTrue($clazzObj instanceof AbstractMiddleware);
- $this->assertEquals(22, $clazzObj->getPriority());
- $this->assertEquals(['global'], $clazzObj->getGroups());
- $this->assertEquals('Check is cool', $clazzObj->getName());
- $this->removeClass($clazz);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php
deleted file mode 100644
index 1b853afc4..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php
+++ /dev/null
@@ -1,174 +0,0 @@
-assertEquals([
- "Migration namespace: Enter = 'App\Database\Migrations'\n",
- "Provide a name for the class that will have migration logic:\n",
- "Does this migration depend on other migrations?(y/N)\n",
- 'Info: New class was created at "'. APP_PATH .'Database'.DS.'Migrations".'."\n",
- ], $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'migration',
- ], [
- "\n",
- $name,
- "n"
- ]));
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Database' . DS . 'Migrations' . DS . $name . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($clazz));
-
- $instance = new $clazz();
- $this->assertInstanceOf(AbstractMigration::class, $instance);
- $this->assertEquals([], $instance->getDependencies());
- $this->assertEquals([], $instance->getEnvironments());
-
- $this->removeClass($clazz);
- }
-
- /**
- * @test
- */
- public function testCreateMigrationWithDependencies() {
- // First create a base migration
- $baseName = 'BaseMigration';
-
- $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'migration',
- ], [
- "\n",
- $baseName,
- "n",
- "n"
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $baseFile = APP_PATH . 'Database' . DS . 'Migrations' . DS . $baseName . '.php';
- $this->assertTrue(file_exists($baseFile), "Base migration file should exist");
-
- // Manually create a dependent migration using DatabaseChangeGenerator to verify it works
- $generator = new \WebFiori\Database\Schema\DatabaseChangeGenerator();
- $generator->setNamespace('App\\Database\\Migrations');
- $generator->setPath(APP_PATH . 'Database' . DS . 'Migrations');
- $generator->createMigration('ManualDependent', [
- \WebFiori\Database\Schema\GeneratorOption::DEPENDENCIES => ['\\App\\Database\\Migrations\\BaseMigration']
- ]);
-
- $manualFile = APP_PATH . 'Database' . DS . 'Migrations' . DS . 'ManualDependent.php';
- $this->assertTrue(file_exists($manualFile));
- $content = file_get_contents($manualFile);
- $this->assertStringContainsString('getDependencies', $content);
- $this->assertStringContainsString('BaseMigration', $content);
-
- require_once $baseFile;
- require_once $manualFile;
- $instance = new \App\Database\Migrations\ManualDependent();
- $this->assertEquals(['App\\Database\\Migrations\\BaseMigration'], $instance->getDependencies());
-
- $this->removeClass('\\App\\Database\\Migrations\\ManualDependent');
- $this->removeClass('\\App\\Database\\Migrations\\'.$baseName);
- }
-
- /**
- * @test
- */
- public function testCreateMigrationWithEnvironments() {
- // Note: DatabaseChangeGenerator doesn't support environments for migrations yet
- // This test is kept for future compatibility
- $this->markTestSkipped('DatabaseChangeGenerator does not support environments for migrations yet');
-
- $name = 'EnvMigration';
- $clazz = '\\App\\Database\\Migrations\\'.$name;
-
- $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'migration',
- ], [
- "\n",
- $name,
- "n",
- "y",
- "dev",
- "y",
- "staging",
- "n"
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Database' . DS . 'Migrations' . DS . $name . '.php';
- require_once $filePath;
- $this->assertTrue(class_exists($clazz));
-
- $instance = new $clazz();
- $this->assertInstanceOf(AbstractMigration::class, $instance);
- $this->assertEquals(['dev', 'staging'], $instance->getEnvironments());
-
- $this->removeClass($clazz);
- }
-
- /**
- * @test
- */
- public function testCreateMigrationWithDefaults() {
- $name = 'DefaultMigration';
- $clazz = '\\App\\Database\\Migrations\\'.$name;
-
- $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'migration',
- '--defaults' => ''
- ], [
- $name
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Database' . DS . 'Migrations' . DS . $name . '.php';
- $this->assertTrue(file_exists($filePath));
- require_once $filePath;
- $this->assertTrue(class_exists($clazz));
-
- $this->removeClass($clazz);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateRESTTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateRESTTest.php
deleted file mode 100644
index dbcad0935..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateRESTTest.php
+++ /dev/null
@@ -1,418 +0,0 @@
-removeAllDBConnections();
-
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'rest'
- ], [
- '0',
- 'SuperUser',
- 'App\\Entity\\super',
- 'y',
- 'n',
- "App\\Database\\super",
- "super_users",
- "A table to hold super users information.",
- "id",
- "int",
- "11",
- "y",
- "y",
- "The unique ID of the super user.",
- "y",
- 'first-name',
- 'varchar',
- '50',
- 'n',
- 'n',
- "\n", // Hit Enter to pick default value (empty default)
- 'n',
- 'No Comment.',
- "y",
- 'is-happy',
- 'bool',
- 'n',
- 'true',
- 'n',
- 'Check if the hero is happy or not.',
- "n",
- 'n',
- "y",
- "App\\Apis\\super"
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals(array_merge([
- "Warning: No database connections found in application configuration.\n",
- "Info: Run the command \"add\" to add connections.\n",
- "Database type:\n",
- "0: mysql\n",
- "1: mssql\n",
- "First thing, we need entity class information.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Entity'\n",
- "Would you like from your entity class to implement the interface JsonI?(Y/n)\n",
- "Would you like to add extra attributes to the entity?(y/N)\n",
- "Now, time to collect database table information.\n",
- "Provide us with a namespace for table class: Enter = 'App\Database'\n",
- "Enter database table name:\n",
- "Enter your optional comment about the table:\n",
- "Now you have to add columns to the table.\n",
- ],
- CreateTableTest::MYSQL_COLS_TYPES,
- [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column auto increment?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- ],
- CreateTableTest::MYSQL_COLS_TYPES,
- [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column unique?(y/N)\n",
- "Enter default value (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- ],
- CreateTableTest::MYSQL_COLS_TYPES,
- [
- "Is this column primary?(y/N)\n",
- "Enter default value (true or false) (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- "Would you like to add foreign keys to the table?(y/N)\n",
- "Would you like to have update methods for every single column?(y/N)\n",
- "Last thing needed is to provide us with namespace for web services: Enter = 'App\\Apis'\n",
- "Creating entity class...\n",
- "Creating database table class...\n",
- "Creating database access class...\n",
- "Writing web services...\n",
- "Done.\n"
- ]), $output);
-
- $tableClazz = '\\App\\Database\\super\\SuperUserTable';
- $entityClazz = '\\App\\Entity\\super\\SuperUser';
- $dbClazz = "\\App\\Database\\super\\SuperUserDB";
- $apiClazzes = [
- '\\App\\Apis\\super\\AddSuperUserService',
- '\\App\\Apis\\super\\DeleteSuperUserService',
- '\\App\\Apis\\super\\GetAllSuperUsersService',
- '\\App\\Apis\\super\\GetSuperUserService',
- '\\App\\Apis\\super\\UpdateSuperUserService',
- '\\App\\Apis\\super\\UpdateFirstNameOfSuperUserService',
- '\\App\\Apis\\super\\UpdateIsHappyOfSuperUserService'
- ];
-
- foreach ($apiClazzes as $clazz) {
- $this->assertTrue(class_exists($clazz));
- $this->assertTrue(File::isFileExist(ROOT_PATH.DS. str_replace('\\', DS, $clazz).'.php'));
- }
- $this->assertTrue(class_exists($tableClazz));
- $this->assertTrue(class_exists($entityClazz));
- $this->assertTrue(class_exists($dbClazz));
-
- foreach ($apiClazzes as $clazz) {
- $this->removeClass($clazz);
- }
- $this->removeClass($tableClazz);
- $this->removeClass($entityClazz);
- $this->removeClass($dbClazz);
- }
-
- /**
- * @test
- */
- public function test01() {
- App::getConfig()->removeAllDBConnections();
-
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'rest'
- ], [
- '0',
- 'SuperUserX',
- 'App\\Entity\\super',
- 'y',
- 'n',
- "App\\Database\\super",
- "super_users",
- "A table to hold super users information.",
- "id",
- "int",
- "11",
- "n",
- 'n',
- "\n", // Hit Enter to pick default value (empty default)
- 'n',
- "The unique ID of the super user.",
- "y",
- 'first-name',
- 'varchar',
- '50',
- 'n',
- 'n',
- "\n", // Hit Enter to pick default value (empty default)
- 'n',
- 'No Comment.',
- "y",
- 'is-happy',
- 'bool',
- 'n',
- 'true',
- 'n',
- 'Check if the hero is happy or not.',
- "n",
- 'n',
- "y",
- "App\\Apis\\super"
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals(array_merge([
- "Warning: No database connections found in application configuration.\n",
- "Info: Run the command \"add\" to add connections.\n","Database type:\n",
- "0: mysql\n",
- "1: mssql\n",
- "First thing, we need entity class information.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Entity'\n",
- "Would you like from your entity class to implement the interface JsonI?(Y/n)\n",
- "Would you like to add extra attributes to the entity?(y/N)\n",
- "Now, time to collect database table information.\n",
- "Provide us with a namespace for table class: Enter = 'App\Database'\n",
- "Enter database table name:\n",
- "Enter your optional comment about the table:\n",
- "Now you have to add columns to the table.\n",
- ],
- CreateTableTest::MYSQL_COLS_TYPES,
- [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column unique?(y/N)\n",
- "Enter default value (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- ],
- CreateTableTest::MYSQL_COLS_TYPES,
- [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column unique?(y/N)\n",
- "Enter default value (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- ],
- CreateTableTest::MYSQL_COLS_TYPES,
- [
- "Is this column primary?(y/N)\n",
- "Enter default value (true or false) (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- "Would you like to add foreign keys to the table?(y/N)\n",
- "Would you like to have update methods for every single column?(y/N)\n",
- "Last thing needed is to provide us with namespace for web services: Enter = 'App\\Apis'\n",
- "Creating entity class...\n",
- "Creating database table class...\n",
- "Creating database access class...\n",
- "Writing web services...\n",
- "Done.\n"
- ]), $output);
-
- $tableClazz = '\\App\\Database\\super\\SuperUserXTable';
- $entityClazz = '\\App\\Entity\\super\\SuperUserX';
- $dbClazz = "\\App\\Database\\super\\SuperUserXDB";
- $apiClazzes = [
- '\\App\\Apis\\super\\AddSuperUserXService',
- '\\App\\Apis\\super\\DeleteSuperUserXService',
- '\\App\\Apis\\super\\GetAllSuperUserXsService',
- '\\App\\Apis\\super\\GetSuperUserXService',
- '\\App\\Apis\\super\\UpdateSuperUserXService',
- '\\App\\Apis\\super\\UpdateFirstNameOfSuperUserXService',
- '\\App\\Apis\\super\\UpdateIsHappyOfSuperUserXService',
- '\\App\\Apis\\super\\UpdateIdOfSuperUserXService'
- ];
-
- foreach ($apiClazzes as $clazz) {
- $this->assertTrue(class_exists($clazz));
- }
- $this->assertTrue(class_exists($tableClazz));
- $this->assertTrue(class_exists($entityClazz));
- $this->assertTrue(class_exists($dbClazz));
-
- foreach ($apiClazzes as $clazz) {
- $this->removeClass($clazz);
- }
- $this->removeClass($tableClazz);
- $this->removeClass($entityClazz);
- $this->removeClass($dbClazz);
- }
-
- /**
- * @test
- */
- public function test02() {
- App::getConfig()->removeAllDBConnections();
- $conn = new ConnectionInfo('mysql','root', '123456', 'testing_db', '127.0.0.1', 3306, [
- 'connection-name' => 'Super Connection'
- ]);
- App::getConfig()->addOrUpdateDBConnection($conn);
-
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'rest',
- ], [
- 'Super Connection',
- 'SuperUserX9',
- 'App\\Entity\\super',
- 'y',
- 'n',
- "App\\Database\\super",
- "super_users",
- "A table to hold super users information.",
- "id",
- "int",
- "11",
- "n",
- 'n',
- "\n", // Hit Enter to pick default value (empty default)
- 'n',
- "The unique ID of the super user.",
- "y",
- 'first-name',
- 'varchar',
- '50',
- 'n',
- 'n',
- "\n", // Hit Enter to pick default value (empty default)
- 'n',
- 'No Comment.',
- "y",
- 'is-happy',
- 'bool',
- 'n',
- 'true',
- 'n',
- 'Check if the hero is happy or not.',
- "n",
- 'n',
- "y",
- "App\\Apis\\super"
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals(array_merge([
- "Select database connection:\n",
- "0: Super Connection <--\n",
- "First thing, we need entity class information.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Entity'\n",
- "Would you like from your entity class to implement the interface JsonI?(Y/n)\n",
- "Would you like to add extra attributes to the entity?(y/N)\n",
- "Now, time to collect database table information.\n",
- "Provide us with a namespace for table class: Enter = 'App\Database'\n",
- "Enter database table name:\n",
- "Enter your optional comment about the table:\n",
- "Now you have to add columns to the table.\n",
- ],
- CreateTableTest::MYSQL_COLS_TYPES,
- [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column unique?(y/N)\n",
- "Enter default value (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- ],
- CreateTableTest::MYSQL_COLS_TYPES,
- [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column unique?(y/N)\n",
- "Enter default value (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n"],
- CreateTableTest::MYSQL_COLS_TYPES,
- ["Is this column primary?(y/N)\n",
- "Enter default value (true or false) (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- "Would you like to add foreign keys to the table?(y/N)\n",
- "Would you like to have update methods for every single column?(y/N)\n",
- "Last thing needed is to provide us with namespace for web services: Enter = 'App\\Apis'\n",
- "Creating entity class...\n",
- "Creating database table class...\n",
- "Creating database access class...\n",
- "Writing web services...\n",
- "Done.\n"
- ]), $output);
-
- $tableClazz = '\\App\\Database\\super\\SuperUserX9Table';
- $entityClazz = '\\App\\Entity\\super\\SuperUserX9';
- $dbClazz = "\\App\\Database\\super\\SuperUserX9DB";
- $apiClazzes = [
- '\\App\\Apis\\super\\AddSuperUserX9Service',
- '\\App\\Apis\\super\\DeleteSuperUserX9Service',
- '\\App\\Apis\\super\\GetAllSuperUserX9sService',
- '\\App\\Apis\\super\\GetSuperUserX9Service',
- '\\App\\Apis\\super\\UpdateSuperUserX9Service',
- '\\App\\Apis\\super\\UpdateFirstNameOfSuperUserX9Service',
- '\\App\\Apis\\super\\UpdateIsHappyOfSuperUserX9Service',
- '\\App\\Apis\\super\\UpdateIdOfSuperUserX9Service',
- '\\App\\Apis\\super\\UpdateIsHappyOfSuperUserX9Service'
- ];
-
- foreach ($apiClazzes as $clazz) {
- $this->assertTrue(class_exists($clazz));
- }
- $this->assertTrue(class_exists($tableClazz));
- $this->assertTrue(class_exists($entityClazz));
- $this->assertTrue(class_exists($dbClazz));
-
- foreach ($apiClazzes as $clazz) {
- $this->removeClass($clazz);
- }
- $this->removeClass($tableClazz);
- $this->removeClass($entityClazz);
- $this->removeClass($dbClazz);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryTest.php
deleted file mode 100644
index c6398100d..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryTest.php
+++ /dev/null
@@ -1,93 +0,0 @@
-cleanupInfrastructure();
- parent::tearDown();
- }
-
- private function cleanupInfrastructure(): void {
- $dir = APP_PATH . 'Infrastructure' . DS . 'Repository';
- if (is_dir($dir)) {
- foreach (glob($dir . DS . '*.php') as $file) {
- unlink($file);
- }
- }
- }
-
- /**
- * @test
- */
- public function testCreateRepository() {
- $output = $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'repository'
- ], [
- "\n", // namespace (use default)
- 'UserRepository', // class name
- 'App\\Domain\\User', // entity class
- 'users', // table name
- 'id', // id field
- 'id', // property name
- '0', // type: int
- 'name', // property name
- '1', // type: string
- 'email', // property name
- '1', // type: string
- "\n" // finish
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Infrastructure' . DS . 'Repository' . DS . 'UserRepository.php';
- $this->assertTrue(file_exists($filePath), 'Repository file should exist');
-
- $content = file_get_contents($filePath);
- $this->assertStringContainsString('namespace App\\Infrastructure\\Repository', $content);
- $this->assertStringContainsString('use WebFiori\\Database\\Repository\\AbstractRepository', $content);
- $this->assertStringContainsString('use App\\Domain\\User', $content);
- $this->assertStringContainsString('class UserRepository extends AbstractRepository', $content);
- $this->assertStringContainsString('function getTableName()', $content);
- $this->assertStringContainsString('return \'users\'', $content);
- $this->assertStringContainsString('function getIdField()', $content);
- $this->assertStringContainsString('return \'id\'', $content);
- $this->assertStringContainsString('function toEntity', $content);
- $this->assertStringContainsString('function toArray', $content);
- $this->assertStringContainsString('new User(', $content);
- }
-
- /**
- * @test
- */
- public function testCreateRepositoryWithDefaults() {
- $output = $this->executeMultiCommand([
- CreateCommand::class,
- '--c' => 'repository',
- '--defaults' => ''
- ], [
- 'ProductRepository',
- 'App\\Domain\\Product',
- 'products',
- 'id'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
-
- $filePath = APP_PATH . 'Infrastructure' . DS . 'Repository' . DS . 'ProductRepository.php';
- $this->assertTrue(file_exists($filePath));
-
- $content = file_get_contents($filePath);
- $this->assertStringContainsString('class ProductRepository', $content);
- $this->assertStringContainsString('return \'products\'', $content);
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateTableTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTableTest.php
deleted file mode 100644
index 0f7d4a28d..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateTableTest.php
+++ /dev/null
@@ -1,279 +0,0 @@
-executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'table'
- ], [
- 'mysql',
- 'Cool00Table',
- "\n", // Hit Enter to pick default value (App\database)
- 'cool_table_00',
- 'This is the first cool table that was created using CLI.',
- 'id',
- '1',
- '11',
- 'y',
- 'y',
- 'The unique ID of the cool thing.',
- 'n',
- 'n',
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertTrue(class_exists('\\App\\Database\\Cool00Table'));
- $clazz = '\\App\\Database\\Cool00Table';
- $this->removeClass($clazz);
- $testObj = new $clazz();
- $this->assertTrue($testObj instanceof MySQLTable);
- $this->assertEquals('`cool_table_00`', $testObj->getName());
- $this->assertEquals('This is the first cool table that was created using CLI.', $testObj->getComment());
- $this->assertEquals(1, $testObj->getColsCount());
- $this->assertEquals([
- 'id'
- ], $testObj->getColsKeys());
- $this->assertEquals([
- '`id`'
- ], $testObj->getColsNames());
-
- $this->assertEquals(array_merge([
- "Database type:\n",
- "0: mysql\n",
- "1: mssql\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Database'\n",
- "Enter database table name: Enter = 'cool_00_table'\n",
- "Enter your optional comment about the table:\n",
- "Now you have to add columns to the table.\n",
- ], self::MYSQL_COLS_TYPES, [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column auto increment?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- "Would you like to add foreign keys to the table?(y/N)\n",
- "Would you like to create an entity class that maps to the database table?(y/N)\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Database\".\n",
- ]), $output);
- }
-
- /**
- * @test
- */
- public function testCreateTable01() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'table'
- ], [
- 'mssql',
- 'Cool01Table',
- "\n", // Hit Enter to pick default value (App\database)
- 'cool_table_01',
- 'This is the first cool table that was created using CLI.',
- 'id',
- '1',
- 'n',
- 'y',
- 'The unique ID of the cool thing.',
- 'n',
- 'n',
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $clazz = '\\App\\Database\\Cool01Table';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass($clazz);
- $testObj = new $clazz();
- $this->assertTrue($testObj instanceof MSSQLTable);
- $this->assertEquals('[cool_table_01]', $testObj->getName());
- $this->assertEquals('This is the first cool table that was created using CLI.', $testObj->getComment());
- $this->assertEquals(1, $testObj->getColsCount());
- $this->assertEquals([
- 'id'
- ], $testObj->getColsKeys());
- $this->assertEquals([
- '[id]'
- ], $testObj->getColsNames());
-
- $col = $testObj->getColByKey('id');
- $this->assertFalse($col->isIdentity());
-
- $this->assertEquals(array_merge([
- "Database type:\n",
- "0: mysql\n",
- "1: mssql\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Database'\n",
- "Enter database table name: Enter = 'cool_01_table'\n",
- "Enter your optional comment about the table:\n",
- "Now you have to add columns to the table.\n",
- ], self::MSSQL_COLS_TYPES, [
- "Is this column an identity column?(y/N)\n",
- "Is this column primary?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- "Would you like to add foreign keys to the table?(y/N)\n",
- "Would you like to create an entity class that maps to the database table?(y/N)\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Database\".\n",
- ]), $output);
- }
-
- /**
- * @test
- */
- public function testCreateTable02() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'table'
- ], [
- 'mysql',
- 'Cool02Table',
- "\n", // Hit Enter to pick default value (App\database)
- 'cool_table_02',
- "\n", // Hit Enter to pick default value (empty comment)
- 'id',
- '1',
- '11',
- 'y',
- 'y',
- 'The unique ID of the cool thing.',
- 'y',
- 'id', // Duplicate column name
- 'y',
- 'name',
- '3',
- '400',
- 'n',
- 'n',
- "\n", // Hit Enter to pick default value (empty default)
- 'n',
- 'The name of the user',
- 'n',
- 'n',
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertTrue(class_exists('\\App\\Database\\Cool02Table'));
- $clazz = '\\App\\Database\\Cool02Table';
- $this->removeClass($clazz);
- $testObj = new $clazz();
- $this->assertTrue($testObj instanceof MySQLTable);
- $this->assertEquals('`cool_table_02`', $testObj->getName());
- $this->assertNull($testObj->getComment());
- $this->assertEquals(2, $testObj->getColsCount());
- $this->assertEquals([
- 'id',
- 'name'
- ], $testObj->getColsKeys());
- $this->assertEquals([
- '`id`',
- '`name`'
- ], $testObj->getColsNames());
-
- $this->assertEquals(array_merge([
- "Database type:\n",
- "0: mysql\n",
- "1: mssql\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Database'\n",
- "Enter database table name: Enter = 'cool_02_table'\n",
- "Enter your optional comment about the table:\n",
- "Now you have to add columns to the table.\n",
- ], self::MYSQL_COLS_TYPES, [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column auto increment?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- "Enter a name for column key:\n",
- "Warning: The table already has a key with name 'id'.\n",
- "Would you like to add another column?(y/N)\n",
- ], self::MYSQL_COLS_TYPES, [
- "Enter column size:\n",
- "Is this column primary?(y/N)\n",
- "Is this column unique?(y/N)\n",
- "Enter default value (Hit \"Enter\" to skip): Enter = ''\n",
- "Can this column have null values?(y/N)\n",
- "Enter your optional comment about the column:\n",
- "Success: Column added.\n",
- "Would you like to add another column?(y/N)\n",
- "Would you like to add foreign keys to the table?(y/N)\n",
- "Would you like to create an entity class that maps to the database table?(y/N)\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Database\".\n",
- ]), $output);
- }
-
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateTaskTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTaskTest.php
deleted file mode 100644
index d09f79541..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateTaskTest.php
+++ /dev/null
@@ -1,229 +0,0 @@
-executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '3',
- 'SuperCoolTask',
- 'App\Tasks',
- 'The Greatest task',
- 'The task will do nothing.',
- 'N',
- "\n", // Hit Enter to pick default value
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Tasks'\n",
- "Enter a name for the task:\n",
- "Provide short description of what does the task will do:\n",
- "Would you like to add arguments to the task?(y/N)\n",
- "Info: New class was created at \"".ROOT_PATH.DS.'App'.DS."Tasks\".\n",
- ], $output);
- $clazz = '\\App\\Tasks\\SuperCoolTask';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass($clazz);
- }
-
- /**
- * @test
- */
- public function test01() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '3',
- 'SuperCoolTask',
- 'App\Tasks',
- 'SuperCool2',
- 'App\Tasks',
- 'The Greatest task',
- 'The task will do nothing.',
- 'N',
- "\n", // Hit Enter to pick default value
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Tasks'\n",
- "Error: A class in the given namespace which has the given name was found.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Tasks'\n",
- "Enter a name for the task:\n",
- "Provide short description of what does the task will do:\n",
- "Would you like to add arguments to the task?(y/N)\n",
- "Info: New class was created at \"".ROOT_PATH.DS.'App'.DS."Tasks\".\n",
- ], $output);
- $clazz = '\\App\\Tasks\\SuperCool2Task';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('\\App\\Tasks\\SuperCoolTask');
- $this->removeClass($clazz);
- }
-
- /**
- * @test
- */
- public function test02() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '3',
- 'NewRound',
- 'App\Tasks',
- '', // Invalid empty name
- 'Invalid#', // Invalid name with special character
- 'Create Round task',
- ' ', // Invalid description (space only)
- ' The task will do nothing. ',
- 'N',
- "\n", // Hit Enter to pick default value
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Tasks'\n",
- "Enter a name for the task:\n",
- "Error: Provided name is invalid!\n",
- "Enter a name for the task:\n",
- "Error: Provided name is invalid!\n",
- "Enter a name for the task:\n",
- "Provide short description of what does the task will do:\n",
- "Error: Invalid input is given. Try again.\n",
- "Provide short description of what does the task will do:\n",
- "Would you like to add arguments to the task?(y/N)\n",
- "Info: New class was created at \"".ROOT_PATH.DS.'App'.DS."Tasks\".\n",
- ], $output);
- $clazz = '\\App\\Tasks\\NewRoundTask';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass($clazz);
- }
-
- /**
- * @test
- */
- public function test03() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'task'
- ], [
- 'SendDailyReport',
- 'App\Tasks',
- 'Send Sales Report',
- 'The task will execute every day to send sales report to management.',
- 'y',
- 'start',
- 'Start date of the report.',
- "\n", // Hit Enter to pick default value (empty default)
- 'y',
- 'end?', // Invalid argument name
- 'y',
- 'end',
- 'End date of the report.',
- '2021-07-07',
- 'y',
- "\n", // Hit Enter to pick default value (invalid empty name)
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\\Tasks'\n",
- "Enter a name for the task:\n",
- "Provide short description of what does the task will do:\n",
- "Would you like to add arguments to the task?(y/N)\n",
- "Enter argument name:\n",
- "Describe the use of the argument: Enter = ''\n",
- "Default value: Enter = ''\n",
- "Would you like to add more arguments?(y/N)\n",
- "Enter argument name:\n",
- "Error: Invalid argument name: end?\n",
- "Would you like to add more arguments?(y/N)\n",
- "Enter argument name:\n",
- "Describe the use of the argument: Enter = ''\n",
- "Default value: Enter = ''\n",
- "Would you like to add more arguments?(y/N)\n",
- "Enter argument name:\n",
- "Error: Invalid argument name: \n",
- "Would you like to add more arguments?(y/N)\n",
- "Info: New class was created at \"".ROOT_PATH.DS.'App'.DS."Tasks\".\n",
- ], $output);
- $clazz = '\\App\\Tasks\\SendDailyReportTask';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass($clazz);
- $task = new $clazz();
- $this->assertTrue($task instanceof AbstractTask);
- $this->assertEquals('Send Sales Report', $task->gettaskName());
- $this->assertEquals('The task will execute every day to send sales report to management.', $task->getDescription());
- $this->assertEquals(2, count($task->getArguments()));
- $arg1 = $task->getArgument('start');
- $this->assertEquals('Start date of the report.', $arg1->getDescription());
- $this->assertNull($arg1->getDefault());
-
- $arg2 = $task->getArgument('end');
- $this->assertEquals('End date of the report.', $arg2->getDescription());
- $this->assertEquals('2021-07-07', $arg2->getDefault());
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateThemeTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateThemeTest.php
deleted file mode 100644
index 30c682b1e..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateThemeTest.php
+++ /dev/null
@@ -1,137 +0,0 @@
-removeDirectory($themePath);
- }
- }
- }
- parent::tearDown();
- }
-
- private function removeDirectory($dir) {
- if (!is_dir($dir)) {
- return;
- }
- $files = array_diff(scandir($dir), ['.', '..']);
- foreach ($files as $file) {
- $path = $dir . DS . $file;
- is_dir($path) ? $this->removeDirectory($path) : unlink($path);
- }
- rmdir($dir);
- } /**
- * @test
- */
- public function testCreateTheme00() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '6',
- 'NewTest',
- 'Themes\\Fiori',
- "\n", // Hit Enter to pick default value
- ]);
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'Themes'\n",
- 'Creating theme at "'.ROOT_PATH.DS.'Themes'.DS."Fiori\"...\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'Themes'.DS."Fiori\".\n",
- ], $output);
-
- $this->assertTrue(class_exists('\\Themes\\Fiori\\NewTestTheme'));
-
- $this->removeClass('\\Themes\\Fiori\\NewTestTheme');
- $this->removeClass('\\Themes\\Fiori\\AsideSection');
- $this->removeClass('\\Themes\\Fiori\\FooterSection');
- $this->removeClass('\\Themes\\Fiori\\HeadSection');
- $this->removeClass('\\Themes\\Fiori\\HeaderSection');
- }
-
- /**
- * @test
- */
- public function testCreateThemeWithExistingName() {
- $runner = App::getRunner();
- $ns = '\\Themes\\FioriTheme';
- $name = 'NewFTestTheme';
-
- $ns2 = '\\Themes\\Cool';
- $name2 = 'NewFTestTheme';
-
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '6',
- $name,
- $ns,
- $name2,
- $ns2,
- "\n" // Hit Enter to pick default value
- ]);
-
- // Verify exact output array for duplicate theme creation attempt
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'Themes'\n",
- "Error: A class in the given namespace which has the given name was found.\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'Themes'\n",
- 'Creating theme at "'.ROOT_PATH.DS.'Themes'.DS."Cool\"...\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'Themes'.DS."Cool\".\n",
- ], $output);
- $this->assertEquals(0, $this->getExitCode());
- $this->removeClass($ns2.'\\'.$name2);
- $this->removeClass($ns2.'\\AsideSection');
- $this->removeClass($ns2.'\\FooterSection');
- $this->removeClass($ns2.'\\HeadSection');
- $this->removeClass($ns2.'\\HeaderSection');
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateWebServiceTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateWebServiceTest.php
deleted file mode 100644
index ffbe44ae9..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/CreateWebServiceTest.php
+++ /dev/null
@@ -1,336 +0,0 @@
-executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create'
- ], [
- '2',
- 'NewWeb',
- "\n", // Hit Enter to pick default value (App\Apis)
- 'get-hello',
- 'Service Desc',
- "\n", // Hit Enter to pick default value (GET method)
- 'n',
- 'y',
- 'name',
- '6',
- 'Random desc',
- 'n',
- 'n',
- 'n',
- "\n", // Hit Enter to pick default value
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to create?\n",
- "0: Database table class.\n",
- "1: Entity class from table.\n",
- "2: Web service.\n",
- "3: Background Task.\n",
- "4: Middleware.\n",
- "5: CLI Command.\n",
- "6: Theme.\n",
- "7: Database access class based on table.\n",
- "8: Complete REST backend (Database table, entity, database access and web services).\n",
- "9: Web service test case.\n",
- "10: Database migration.\n",
- "11: Quit. <--\n",
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Apis'\n",
- "Enter a name for the new web service:\n",
- "Description:\n",
- "Request method:\n",
- "0: CONNECT\n",
- "1: DELETE\n",
- "2: GET <--\n",
- "3: HEAD\n",
- "4: OPTIONS\n",
- "5: PATCH\n",
- "6: POST\n",
- "7: PUT\n",
- "8: TRACE\n",
- "Would you like to add another request method?(y/N)\n",
- "Would you like to add request parameters to the service?(y/N)\n",
- "Enter a name for the request parameter:\n",
- "Choose parameter type:\n",
- "0: array <--\n",
- "1: boolean\n",
- "2: email\n",
- "3: double\n",
- "4: integer\n",
- "5: json-obj\n",
- "6: string\n",
- "7: url\n",
- "Description:\n",
- "Is this parameter optional?(Y/n)\n",
- "Are empty values allowed?(y/N)\n",
- "Would you like to set minimum and maximum length?(y/N)\n",
- "Success: New parameter added.\n",
- "Would you like to add another parameter?(y/N)\n",
- "Creating the class...\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Apis\".\n",
- "Info: Don't forget to add the service to a services manager.\n",
- ], $output);
-
- $clazz = '\\App\\Apis\\NewWebService';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('\\App\\Apis\\NewWebService');
- $instance = new $clazz();
- $instance instanceof AbstractWebService;
- $this->assertEquals('get-hello', $instance->getName());
- $this->assertEquals(1, count($instance->getParameters()));
- $this->assertEquals('Service Desc', $instance->getDescription());
- $this->assertEquals([RequestMethod::GET], $instance->getRequestMethods());
- $param00 = $instance->getParameters()[0];
- $this->assertRequestParameter($param00, [
- ParamOption::NAME => 'name',
- ParamOption::TYPE => ParamType::STRING,
- ParamOption::DESCRIPTION => 'Random desc',
- ParamOption::DEFAULT => null,
- ParamOption::EMPTY => false,
- ParamOption::MAX => null,
- ParamOption::MAX_LENGTH => null,
- ParamOption::MIN => null,
- ParamOption::MIN_LENGTH => null,
- ParamOption::OPTIONAL => false,
- ]);
- }
-
- /**
- * @test
- */
- public function test01() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'web-service'
- ], [
- 'NewWeb2',
- "\n", // Hit Enter to pick default value (App\Apis)
- 'get-hello-2',
- 'Service\'s Desc',
- "\n", // Hit Enter to pick default value (GET method)
- 'y',
- '6',
- 'n',
- 'y',
- 'a-number',
- '3',
- 'Random\'s desc',
- 'n',
- "\n", // Hit Enter to pick default value
- "\n", // Hit Enter to pick default value
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Apis'\n",
- "Enter a name for the new web service:\n",
- "Description:\n",
- "Request method:\n",
- "0: CONNECT\n",
- "1: DELETE\n",
- "2: GET <--\n",
- "3: HEAD\n",
- "4: OPTIONS\n",
- "5: PATCH\n",
- "6: POST\n",
- "7: PUT\n",
- "8: TRACE\n",
- "Would you like to add another request method?(y/N)\n",
- "Request method:\n",
- "0: CONNECT\n",
- "1: DELETE\n",
- "2: GET <--\n",
- "3: HEAD\n",
- "4: OPTIONS\n",
- "5: PATCH\n",
- "6: POST\n",
- "7: PUT\n",
- "8: TRACE\n",
- "Would you like to add another request method?(y/N)\n",
- "Would you like to add request parameters to the service?(y/N)\n",
- "Enter a name for the request parameter:\n",
- "Choose parameter type:\n",
- "0: array <--\n",
- "1: boolean\n",
- "2: email\n",
- "3: double\n",
- "4: integer\n",
- "5: json-obj\n",
- "6: string\n",
- "7: url\n",
- "Description:\n",
- "Is this parameter optional?(Y/n)\n",
- "Would you like to set minimum and maximum limites?(y/N)\n",
- "Success: New parameter added.\n",
- "Would you like to add another parameter?(y/N)\n",
- "Creating the class...\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Apis\".\n",
- "Info: Don't forget to add the service to a services manager.\n",
- ], $output);
-
- $clazz = '\\App\\Apis\\NewWeb2Service';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass('\\App\\Apis\\NewWeb2Service');
- $instance = new $clazz();
- $instance instanceof AbstractWebService;
- $this->assertEquals('get-hello-2', $instance->getName());
- $this->assertEquals(1, count($instance->getParameters()));
- $this->assertEquals('Service\'s Desc', $instance->getDescription());
- $this->assertEquals([RequestMethod::GET, RequestMethod::POST], $instance->getRequestMethods());
- $param00 = $instance->getParameters()[0];
- $this->assertRequestParameter($param00, [
- ParamOption::NAME => 'a-number',
- ParamOption::TYPE => ParamType::DOUBLE,
- ParamOption::DESCRIPTION => 'Random\'s desc',
- ParamOption::DEFAULT => null,
- ParamOption::EMPTY => false,
- ParamOption::MAX => 1e50,
- ParamOption::MAX_LENGTH => null,
- ParamOption::MIN => -1e50,
- ParamOption::MIN_LENGTH => null,
- ParamOption::OPTIONAL => false,
- ]);
- }
-
- /**
- * @test
- */
- public function test02() {
- $output = $this->executeSingleCommand(new CreateCommand(), [
- 'WebFiori',
- 'create',
- '--c' => 'web-service'
- ], [
- 'NewWeb3',
- "\n", // Hit Enter to pick default value (App\Apis)
- 'get-hello-3',
- 'Service\'s Desc',
- "\n", // Hit Enter to pick default value (GET method)
- 'y',
- '6',
- 'n',
- 'y',
- 'a-number',
- '4',
- 'Random\'s desc',
- 'n',
- "\n", // Hit Enter to pick default value
- "\n", // Hit Enter to pick default value
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Enter a name for the new class:\n",
- "Enter an optional namespace for the class: Enter = 'App\Apis'\n",
- "Enter a name for the new web service:\n",
- "Description:\n",
- "Request method:\n",
- "0: CONNECT\n",
- "1: DELETE\n",
- "2: GET <--\n",
- "3: HEAD\n",
- "4: OPTIONS\n",
- "5: PATCH\n",
- "6: POST\n",
- "7: PUT\n",
- "8: TRACE\n",
- "Would you like to add another request method?(y/N)\n",
- "Request method:\n",
- "0: CONNECT\n",
- "1: DELETE\n",
- "2: GET <--\n",
- "3: HEAD\n",
- "4: OPTIONS\n",
- "5: PATCH\n",
- "6: POST\n",
- "7: PUT\n",
- "8: TRACE\n",
- "Would you like to add another request method?(y/N)\n",
- "Would you like to add request parameters to the service?(y/N)\n",
- "Enter a name for the request parameter:\n",
- "Choose parameter type:\n",
- "0: array <--\n",
- "1: boolean\n",
- "2: email\n",
- "3: double\n",
- "4: integer\n",
- "5: json-obj\n",
- "6: string\n",
- "7: url\n",
- "Description:\n",
- "Is this parameter optional?(Y/n)\n",
- "Would you like to set minimum and maximum limites?(y/N)\n",
- "Success: New parameter added.\n",
- "Would you like to add another parameter?(y/N)\n",
- "Creating the class...\n",
- 'Info: New class was created at "'.ROOT_PATH.DS.'App'.DS."Apis\".\n",
- "Info: Don't forget to add the service to a services manager.\n",
- ], $output);
-
- $clazz = '\\App\\Apis\\NewWeb3Service';
- $this->assertTrue(class_exists($clazz));
- $this->removeClass($clazz);
- $instance = new $clazz();
- $instance instanceof AbstractWebService;
- $this->assertEquals('get-hello-3', $instance->getName());
- $this->assertEquals(1, count($instance->getParameters()));
- $this->assertEquals('Service\'s Desc', $instance->getDescription());
- $this->assertEquals([RequestMethod::GET, RequestMethod::POST], $instance->getRequestMethods());
- $param00 = $instance->getParameters()[0];
- $this->assertRequestParameter($param00, [
- ParamOption::NAME => 'a-number',
- ParamOption::TYPE => ParamType::INT,
- ParamOption::DESCRIPTION => 'Random\'s desc',
- ParamOption::DEFAULT => null,
- ParamOption::EMPTY => false,
- ParamOption::MAX => PHP_INT_MAX,
- ParamOption::MAX_LENGTH => null,
- ParamOption::MIN => defined('PHP_INT_MIN') ? PHP_INT_MIN : ~PHP_INT_MAX,
- ParamOption::MIN_LENGTH => null,
- ParamOption::OPTIONAL => false,
- ]);
- }
-
- private function assertRequestParameter(RequestParameter $param, array $expected) {
- $this->assertEquals($expected[ParamOption::NAME], $param->getName());
- $this->assertEquals($expected[ParamOption::TYPE], $param->getType());
- $this->assertEquals($expected[ParamOption::DESCRIPTION], $param->getDescription());
- $this->assertEquals($expected[ParamOption::MIN_LENGTH], $param->getMinLength());
- $this->assertEquals($expected[ParamOption::MAX_LENGTH], $param->getMaxLength());
-
- // Compare MIN/MAX as strings for DOUBLE to avoid PHPUnit Exporter issues with large floats
- if ($param->getType() == ParamType::DOUBLE) {
- $this->assertEquals((string)$expected[ParamOption::MIN], (string)$param->getMinValue());
- $this->assertEquals((string)$expected[ParamOption::MAX], (string)$param->getMaxValue());
- } else {
- $this->assertEquals($expected[ParamOption::MIN], $param->getMinValue());
- $this->assertEquals($expected[ParamOption::MAX], $param->getMaxValue());
- }
-
- $this->assertEquals($expected[ParamOption::EMPTY], $param->isEmptyStringAllowed());
- $this->assertEquals($expected[ParamOption::OPTIONAL], $param->isOptional());
- $this->assertEquals($expected[ParamOption::DEFAULT], $param->getDefault());
- }
-}
From 3a09c017d6a98050fdfa91a27398f74fecb8d696 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 27 Jan 2026 00:28:21 +0300
Subject: [PATCH 35/88] refactor: Locating `ClassLoader`
---
WebFiori/Framework/App.php | 35 ++++++++++++++++++++---------------
1 file changed, 20 insertions(+), 15 deletions(-)
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 627a267d4..0b8954e65 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -384,6 +384,7 @@ public static function initiate(string $appFolder = 'App', string $publicFolder
*/
define('PUBLIC_FOLDER', $publicFolder);
}
+
if (!defined('WF_CORE_PATHS')) {
/**
* Possible Paths to WebFiori's core library.
@@ -444,7 +445,6 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\VersionCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\SchedulerCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\AddCommand',
@@ -651,23 +651,28 @@ private static function initAutoLoader() {
/**
* Initialize autoloader.
*/
- if (!class_exists('WebFiori\Framework\Autoload\ClassLoader',false)) {
- foreach (WF_CORE_PATHS as $path) {
- $autoloader = $path.DIRECTORY_SEPARATOR.'Autoload'.DIRECTORY_SEPARATOR.'ClassLoader.php';
+ if (class_exists('WebFiori\Framework\Autoload\ClassLoader', false)) {
+ return;
+ }
+ $isLoaded = false;
- if (file_exists($autoloader)) {
- require_once $autoloader;
- self::$AU = ClassLoader::get();
- }
- if (!class_exists(APP_DIR.'\\Init\\InitAutoLoad')) {
- Ini::createAppDirs();
- Ini::get()->createIniClass('InitAutoLoad', 'Add user-defined directories to the set of directories at which the framework will search for classes.');
- }
- self::call(APP_DIR.'\\Init\\InitAutoLoad::init');
- return;
+ foreach (WF_CORE_PATHS as $path) {
+ $autoloader = $path.DIRECTORY_SEPARATOR.'Autoload'.DIRECTORY_SEPARATOR.'ClassLoader.php';
+
+ if (file_exists($autoloader)) {
+ require_once $autoloader;
+ self::$AU = ClassLoader::get();
+ $isLoaded = true;
+ }
+ if (!class_exists(APP_DIR.'\\Init\\InitAutoLoad')) {
+ Ini::createAppDirs();
+ Ini::get()->createIniClass('InitAutoLoad', 'Add user-defined directories to the set of directories at which the framework will search for classes.');
}
+ self::call(APP_DIR.'\\Init\\InitAutoLoad::init');
+ }
+ if (!$isLoaded) {
+ throw new \Exception('Unable to locate the autoloader class.');
}
- throw new \Exception('Unable to locate the autoloader class.');
}
/**
* Initialize global constants which has information about framework version.
From 128ee8fda941d619ad06c1cc823d88122cf6a2a6 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 27 Jan 2026 00:30:04 +0300
Subject: [PATCH 36/88] refactor: Fix `writeConstructor`
---
WebFiori/Framework/Writers/CommandClassWriter.php | 5 ++++-
WebFiori/Framework/Writers/MiddlewareClassWriter.php | 5 ++++-
WebFiori/Framework/Writers/SchedulerTaskClassWriter.php | 5 ++++-
WebFiori/Framework/Writers/TableClassWriter.php | 5 ++++-
WebFiori/Framework/Writers/WebServiceWriter.php | 5 ++++-
5 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/WebFiori/Framework/Writers/CommandClassWriter.php b/WebFiori/Framework/Writers/CommandClassWriter.php
index cbbb39649..41576558e 100644
--- a/WebFiori/Framework/Writers/CommandClassWriter.php
+++ b/WebFiori/Framework/Writers/CommandClassWriter.php
@@ -169,7 +169,10 @@ public function writeClassComment() {
public function writeClassDeclaration() {
$this->append('class '.$this->getName().' extends Command {');
}
- private function writeConstructor() {
+ protected function writeConstructor(array $params = [],
+ $body = '',
+ string $description = 'Creates new instance of the class.',
+ int $indent = 1) {
$this->append([
'/**',
' * Creates new instance of the class.',
diff --git a/WebFiori/Framework/Writers/MiddlewareClassWriter.php b/WebFiori/Framework/Writers/MiddlewareClassWriter.php
index 3eb89a583..68eaa80f6 100644
--- a/WebFiori/Framework/Writers/MiddlewareClassWriter.php
+++ b/WebFiori/Framework/Writers/MiddlewareClassWriter.php
@@ -184,7 +184,10 @@ public function writeClassComment() {
public function writeClassDeclaration() {
$this->append('class '.$this->getName().' extends AbstractMiddleware {');
}
- private function writeConstructor() {
+ protected function writeConstructor(array $params = [],
+ $body = '',
+ string $description = 'Creates new instance of the class.',
+ int $indent = 1) {
$this->append([
'/**',
' * Creates new instance of the class.',
diff --git a/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php b/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
index e4438c8b3..83e3c359f 100644
--- a/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
+++ b/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
@@ -195,7 +195,10 @@ public function writeClassComment() {
public function writeClassDeclaration() {
$this->append('class '.$this->getName().' extends AbstractTask {');
}
- private function writeConstructor() {
+ protected function writeConstructor(array $params = [],
+ $body = '',
+ string $description = 'Creates new instance of the class.',
+ int $indent = 1) {
$this->append([
'/**',
' * Creates new instance of the class.',
diff --git a/WebFiori/Framework/Writers/TableClassWriter.php b/WebFiori/Framework/Writers/TableClassWriter.php
index 911fc32a0..0a7a945dd 100644
--- a/WebFiori/Framework/Writers/TableClassWriter.php
+++ b/WebFiori/Framework/Writers/TableClassWriter.php
@@ -510,7 +510,10 @@ private function getType(string $dataType) {
}
}
}
- private function writeConstructor() {
+ protected function writeConstructor(array $params = [],
+ $body = '',
+ string $description = 'Creates new instance of the class.',
+ int $indent = 1) {
$this->append([
"/**",
" * Creates new instance of the class.",
diff --git a/WebFiori/Framework/Writers/WebServiceWriter.php b/WebFiori/Framework/Writers/WebServiceWriter.php
index d659e812b..c01238b92 100644
--- a/WebFiori/Framework/Writers/WebServiceWriter.php
+++ b/WebFiori/Framework/Writers/WebServiceWriter.php
@@ -313,7 +313,10 @@ private function implementMethods() {
}
$this->append('}', 1);
}
- private function writeConstructor() {
+ protected function writeConstructor(array $params = [],
+ $body = '',
+ string $description = 'Creates new instance of the class.',
+ int $indent = 1) {
$this->append([
"/**",
" * Creates new instance of the class.",
From 39c6391dd10f601ab5ea2b3df16425cc99c2cc39 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 00:06:05 +0300
Subject: [PATCH 37/88] refactor: No Longer Needed
---
.../Framework/Writers/APITestCaseWriter.php | 291 ---------
WebFiori/Framework/Writers/DBClassWriter.php | 563 ------------------
.../Framework/Writers/TableClassWriter.php | 532 -----------------
.../Framework/Writers/WebServiceWriter.php | 351 -----------
4 files changed, 1737 deletions(-)
delete mode 100644 WebFiori/Framework/Writers/APITestCaseWriter.php
delete mode 100644 WebFiori/Framework/Writers/DBClassWriter.php
delete mode 100644 WebFiori/Framework/Writers/TableClassWriter.php
delete mode 100644 WebFiori/Framework/Writers/WebServiceWriter.php
diff --git a/WebFiori/Framework/Writers/APITestCaseWriter.php b/WebFiori/Framework/Writers/APITestCaseWriter.php
deleted file mode 100644
index 770b7c2a2..000000000
--- a/WebFiori/Framework/Writers/APITestCaseWriter.php
+++ /dev/null
@@ -1,291 +0,0 @@
-setSuffix('Test');
-
- if ($manager !== null) {
- $this->setServicesManager($manager);
- }
- $this->setPhpUnitVersion(9);
-
- if (!($service instanceof AbstractWebService)) {
- if ($service !== null && class_exists($service)) {
- $s = new $service();
-
- if ($s instanceof AbstractWebService) {
- $this->setService($s);
- }
- }
- } else {
- $this->setService($service);
- }
- }
- /**
- * Returns PHPUnit version number.
- *
- * This is used to check if annotations or attributes should be used in test case
- * method declaration. Starting with PHPUnit 10, attributes are used.
- *
- * @return int
- */
- public function getPhpUnitVersion() : int {
- return $this->phpunitV;
- }
- /**
- * Returns the web service object which was associated with the writer.
- *
- * @return AbstractWebService
- */
- public function getService() : AbstractWebService {
- return $this->serviceObj;
- }
- /**
- * Returns the name of the class of the web service.
- *
- * @return string
- */
- public function getServiceName() : string {
- return $this->serviceObjName;
- }
- /**
- * Returns the associated web services manager that will be used by the test case.
- *
- * @return WebServicesManager
- */
- public function getServicesManager() : WebServicesManager {
- return $this->servicesManager;
- }
- /**
- * Returns the name of web services manager class.
- *
- * @return string
- */
- public function getServicesManagerName() : string {
- return $this->servicesManagerName;
- }
- /**
- * Sets PHPUnit version number.
- *
- * This is used to check if annotations or attributes should be used in test case
- * method declaration. Starting with PHPUnit 10, attributes are used.
- *
- * @param int $num
- */
- public function setPhpUnitVersion(int $num) {
- $this->phpunitV = $num;
- }
-
- /**
- * Sets the web service that the writer will use in writing the test case.
- *
- * @param AbstractWebService $service
- */
- public function setService(AbstractWebService $service) {
- $this->serviceObj = $service;
- $clazzExp = explode('\\', get_class($service));
- $this->serviceObjName = $clazzExp[count($clazzExp) - 1];
- }
- /**
- * Sets the associated web services manager that will be used by the test case.
- *
- * @param WebServicesManager $m
- */
- public function setServicesManager(WebServicesManager $m) {
- $this->servicesManager = $m;
- $clazzExp = explode('\\', get_class($m));
- $this->servicesManagerName = $clazzExp[count($clazzExp) - 1];
- }
-
- /**
- * Write the test case class.
- *
- */
- public function writeClass() {
- $this->addAllUse();
- parent::writeClass();
- }
- public function writeClassBody() {
- $this->writeNotAllowedRequestMethodTestCases();
- $this->writeRequiredParametersTestCases();
- $this->writeTestCases();
- $this->append('}');
- }
-
- public function writeClassComment() {
- $this->append("/**\n"
- ." * A unit test class which is used to test the API '".$this->getService()->getName()."'.\n"
- );
- $this->append(" */");
- }
-
- public function writeClassDeclaration() {
- $this->append('class '.$this->getName().' extends APITestCase {');
- }
-
- private function addAllUse() {
- $this->addUseStatement(APITestCase::class);
- $this->addUseStatement(RequestMethod::class);
- $this->addUseStatement(get_class($this->getService()));
- $this->addUseStatement(get_class($this->getServicesManager()));
- $this->addUseStatement('PHPUnit\Framework\Attributes\Test');
- }
- private function addTestAnnotation() {
- if ($this->getPhpUnitVersion() >= 10) {
- $this->append('#[Test]', 1);
- } else {
- $this->append('/**', 1);
- $this->append(' * @test', 1);
- $this->append(' */', 1);
- }
- }
- private function getMethName($reqMeth) {
- if ($reqMeth == RequestMethod::GET) {
- return 'getRequest';
- } else {
- if ($reqMeth == RequestMethod::PUT) {
- return 'putRequest';
- } else {
- if ($reqMeth == RequestMethod::POST) {
- return 'postRequest';
- } else {
- if ($reqMeth == RequestMethod::DELETE) {
- return 'getRequest';
- } else {
- return 'callEndpoint';
- }
- }
- }
- }
- }
- private function writeNotAllowedRequestMethodTestCases() {
- $methods = $this->getService()->getRequestMethods();
- $testCasesCount = 0;
-
- foreach (RequestMethod::getAll() as $method) {
- if (!in_array($method, $methods)) {
- $this->addTestAnnotation();
- $this->append('public function testRequestMethodNotAllowed'.($testCasesCount < 10 ? '0'.$testCasesCount : $testCasesCount).'() {', 1);
- $methodName = $this->getMethName($method);
-
- if ($methodName == 'callEndpoint') {
- $this->append('$output = $this->'.$methodName.'(new '.$this->getServicesManagerName().'(), RequestMethod::'.strtoupper($method).', '.$this->getServiceName().'::class, []);', 2);
- } else {
- $this->append('$output = $this->'.$methodName.'(new '.$this->getServicesManagerName().'(), '.$this->getServiceName().'::class, []);', 2);
- }
- $this->append("\$this->assertEquals('{'.self::NL", 2);
- $this->append(". ' \"message\":\"Method Not Allowed.\",'.self::NL", 2);
- $this->append(". ' \"type\":\"error\",'.self::NL", 2);
- $this->append(". ' \"http_code\":405'.self::NL", 2);
- $this->append(". '}', \$output);", 2);
- $this->append('}', 1);
- $testCasesCount++;
- }
- }
- }
- private function writeRequiredParametersTestCases() {
- $params = $this->getService()->getParameters();
- $responseMessage = ResponseMessage::get('404-2');
- $missingArr = [];
-
- foreach ($params as $param) {
- if (!$param->isOptional()) {
- $missingArr[] = $param->getName();
- }
- }
-
- if (count($missingArr) !== 0) {
- $requestMethod = $this->getService()->getRequestMethods()[0];
- $this->addTestAnnotation();
- $this->append('public function testRequiredParameters() {', 1);
- $this->append('$output = $this->callEndpoint(new '.$this->getServicesManagerName().'(), RequestMethod::'.strtoupper($requestMethod).', '.$this->getServiceName().'::class, []);', 2);
- $this->append("\$this->assertEquals('{'.self::NL", 2);
- $this->append(". ' \"message\":\"$responseMessage\'".implode("\',", $missingArr)."\'.\",'.self::NL", 2);
- $this->append(". ' \"type\":\"error\",'.self::NL", 2);
- $this->append(". ' \"http_code\":404,'.self::NL", 2);
- $this->append(". ' \"more_info\":{'.self::NL", 2);
- $this->append(". ' \"missing\":['.self::NL", 2);
-
- for ($x = 0 ; $x < count($missingArr) ; $x++) {
- $item = $missingArr[$x];
-
- if ($x + 1 == count($missingArr)) {
- $this->append(". ' \"$item\"'.self::NL", 2);
- } else {
- $this->append(". ' \"$item\",'.self::NL", 2);
- }
- }
- $this->append(". ' ]'.self::NL", 2);
- $this->append(". ' }'.self::NL", 2);
- $this->append(". '}', \$output);", 2);
- $this->append('}', 1);
- }
- }
- private function writeTestCases() {
- $methods = $this->getService()->getRequestMethods();
- $testCasesCount = 0;
-
- foreach (RequestMethod::getAll() as $method) {
- if (in_array($method, $methods)) {
- $this->addTestAnnotation();
- $this->append('public function test'.$method.'Request00() {', 1);
- $this->append("//TODO: Write test case for $method request.", 2);
- $methodName = $this->getMethName($method);
-
- if (count($this->getService()->getParameters()) == 0) {
- if ($methodName == 'callEndpoint') {
- $this->append('$output = $this->'.$methodName.'(new '.$this->getServicesManagerName().'(), RequestMethod::'.strtoupper($method).', '.$this->getServiceName().'::class, []);', 2);
- } else {
- $this->append('$output = $this->'.$methodName.'(new '.$this->getServicesManagerName().'(), '.$this->getServiceName().'::class, []);', 2);
- }
- } else {
- if ($methodName == 'callEndpoint') {
- $this->append('$output = $this->'.$methodName.'(new '.$this->getServicesManagerName().'(), RequestMethod::'.strtoupper($method).', '.$this->getServiceName().'::class, [', 2);
- } else {
- $this->append('$output = $this->'.$methodName.'(new '.$this->getServicesManagerName().'(), '.$this->getServiceName().'::class, [', 2);
- }
-
- foreach ($this->getService()->getParameters() as $reqParam) {
- $this->append("'".$reqParam->getName()."' => null,", 3);
- }
- $this->append(']);', 2);
- }
-
- $this->append("\$this->assertEquals('{'.self::NL", 2);
- $this->append(". '}', \$output);", 2);
- $this->append('}', 1);
- $testCasesCount++;
- }
- }
- }
-}
diff --git a/WebFiori/Framework/Writers/DBClassWriter.php b/WebFiori/Framework/Writers/DBClassWriter.php
deleted file mode 100644
index 7fd187896..000000000
--- a/WebFiori/Framework/Writers/DBClassWriter.php
+++ /dev/null
@@ -1,563 +0,0 @@
-setTable($table);
- }
- $this->addUseStatement(DB::class);
- $this->includeUpdate = false;
- }
- /**
- * Returns the name of the connection at which the generated class will use to connect
- * to database.
- *
- * @return string|null The name of the connection. If not set, null is returned.
- */
- public function getConnectionName() {
- return $this->connName;
- }
- /**
- * Returns the name of the entity at which the class will use to map records.
- *
- * The name of the entity is taken from entity mapper which is associated
- * with the table at which database operations are based on.
- *
- * @return string Class name of the entity.
- */
- public function getEntityName() : string {
- return $this->entityName;
- }
- /**
- * Returns the table instance at which the class will build
- * database operations based on.
- *
- * @return Table|null If the table is set, it will be returned as an object.
- * If not set, null is returned.
- */
- public function getTable() {
- return $this->associatedTable;
- }
- /**
- * Returns an array that contains the keys of columns which are set as primary
- * unique or identity.
- *
- * Note that if the table has identity column, only the key of this column
- * is returned. Other than that, the keys of the primary columns and
- * unique columns are returned.
- *
- * @return array An array that contains the keys of columns which are set as primary
- * or unique.
- */
- public function getUniqueColsKeys() : array {
- $table = $this->getTable();
-
- if ($table instanceof MSSQLTable && $table->hasIdentity()) {
- $cols = [];
-
- foreach ($table->getCols() as $key => $col) {
- if ($col->isIdentity()) {
- $cols[] = $key;
- break;
- }
- }
-
- return $cols;
- }
-
- $recordUniqueCols = $table->getPrimaryKeyColsKeys();
-
- if (count($recordUniqueCols) == 0) {
- $recordUniqueCols = $table->getUniqueColsKeys();
- }
-
- return $recordUniqueCols;
- }
- /**
- * Include update methods for each single column in the table that
- * is not unique.
- *
- * If this method is called, the writer will write one method for every
- * column in the table to update its value.
- */
- public function includeColumnsUpdate() {
- $this->includeUpdate = true;
- }
- /**
- * Checks if each non-unique table column will have its own update method.
- *
- * @return bool If each column will have its own update method, true is
- * returned. False otherwise.
- */
- public function isColumnUpdateIncluded() : bool {
- return $this->includeUpdate;
- }
- /**
- * Sets the name of the connection at which the generated class will use to connect
- * to database.
- *
- * @param string $connName The name of the connection as it was set in the
- * class 'AppConfig' of the application.
- */
- public function setConnection(string $connName) {
- $trimmed = trim($connName);
-
- if (strlen($trimmed) != 0) {
- $this->connName = $trimmed;
- }
- }
- /**
- * Sets the table at which the class will create logic to perform operations
- * on.
- *
- * @param Table $t
- */
- public function setTable(Table $t) {
- $temp = $this->getTable();
-
- if ($temp !== null) {
- $this->removeUseStatement($temp->getEntityMapper()->getEntityName(true));
- }
- $this->associatedTable = $t;
- $mapper = $t->getEntityMapper();
- $this->entityName = $mapper->getEntityName();
- $this->addUseStatement($mapper->getNamespace().'\\'.$mapper->getEntityName());
- $this->createParamsAndWhereArr();
- }
- /**
- * Maps key name to entity method name.
- *
- * @param string $colKey The name of column key such as 'user-id'.
- *
- * @param string $prefix The type of the method. This one can have only two values,
- * 's' for setter method and 'g' for getter method. Default is 'g'.
- *
- * @return string The name of the mapped method name. If the passed column
- * key is empty string, the method will return empty string.
- *
- * @since 1.0
- */
- public static function toMethodName(string $colKey, $prefix = 'g') {
- $trimmed = trim($colKey);
-
-
- $split = explode('-', $trimmed);
- $methodName = '';
-
- foreach ($split as $namePart) {
- if (strlen($namePart) == 1) {
- $methodName .= strtoupper($namePart);
- } else {
- $firstChar = $namePart[0];
- $methodName .= strtoupper($firstChar).substr($namePart, 1);
- }
- }
-
- return $prefix.$methodName;
- }
- /**
- * Writes the body of the class.
- */
- public function writeClassBody() {
- $this->append([
- 'private static $instance;',
- '/**',
- ' * Returns an instance of the class.',
- ' * ',
- ' * Calling this method multiple times will return same instance.',
- ' * ',
- ' * @return '.$this->getName().' An instance of the class.',
- ' */',
- 'public static function get() : '.$this->getName().' {'
- ], 1);
- $this->append('');
- $this->append('if (self::$instance === null) {', 2);
- $this->append('self::$instance = new '.$this->getName().'();', 3);
- $this->append('}', 2);
- $this->append('');
- $this->append('return self::$instance;', 2);
- $this->append('}', 1);
- $this->append([
- "/**",
- " * Creates new instance of the class.",
- " */",
- $this->f('__construct')
- ], 1);
-
- if ($this->getConnectionName() !== null) {
- $this->append([
- "parent::__construct('".$this->getConnectionName()."');",
- ], 2);
- } else {
- $this->append([
- '//TODO: Specify the name of database connection to use in performing operations.',
- "parent::__construct('');",
- ], 2);
- }
- $this->append([
- "\$this->register('".str_replace("\\", "\\\\", $this->getNamespace())."');",
- ], 2);
- $this->append('}', 1);
-
- $this->writeAddRecord();
-
- $this->writeDeleteRecord();
- $this->writeGetRecord();
- $this->writeGetRecords();
- $this->writeGetRecordsCount();
- $this->writeUpdateRecord();
-
- if ($this->isColumnUpdateIncluded()) {
- $this->writeUpdateRecordMethods();
- }
-
- $this->append('}', 0);
- }
- /**
- * Writes the comment that will appear at the top of the class.
- */
- public function writeClassComment() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $this->append([
- "/**",
- " * A class which is used to perform operations on the table '".$t->getNormalName()."'",
- " */"
- ]);
- }
- /**
- * Writes the string that represent class declaration.
- */
- public function writeClassDeclaration() {
- $this->append('class '.$this->getName().' extends DB {');
- }
- private function createParamsAndWhereArr() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $cols = $this->getUniqueColsKeys();
- $this->paramsArr = [];
- $this->whereArr = [];
-
- foreach ($cols as $key) {
- $colObj = $t->getColByKey($key);
- $this->paramsArr[$colObj->getNormalName()] = $colObj->getPHPType();
- $this->whereArr[] = count($this->whereArr) == 0 ? "->where('$key', $".$colObj->getNormalName().")"
- : "->andWhere('$key', $".$colObj->getNormalName().")";
- }
- }
-
- private function writeAddRecord() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $this->append([
- "/**",
- " * Adds new record to the table '".$t->getNormalName()."'.",
- " *",
- " * @param ".$this->getEntityName().' $entity An object that holds record information.',
- " */",
- $this->f('add'.$this->getEntityName(), ['entity' => $this->getEntityName()])
- ], 1);
- $recordsArr = [];
-
- foreach ($t->getEntityMapper()->getGettersMap(true) as $methName => $col) {
- $colObj = $t->getColByKey($col);
-
- if ($colObj instanceof MSSQLColumn) {
- if (!$colObj->isIdentity() && !($colObj->getDatatype() == 'datetime2' && $colObj->getDefault() !== null)) {
- $recordsArr[] = "'$col' => \$entity->$methName(),";
- }
- } else {
- if (!$colObj->isAutoInc() && !($colObj->getDatatype() == 'timestamp' && $colObj->getDefault() !== null)) {
- $recordsArr[] = "'$col' => \$entity->$methName(),";
- }
- }
- }
- $this->append([
- "\$this->table('".$t->getNormalName()."')->insert(["
- ], 2);
- $this->append($recordsArr, 3);
- $this->append('])->execute();', 2);
-
- $this->append('}', 1);
- }
- private function writeColUpdate(Column $colObj, $key) {
- $phpType = $colObj->getPHPType();
- $t = $this->getTable();
- $this->append([
- "/**",
- " * Updates the value of the column '".$colObj->getNormalName()."' on the table '".$t->getNormalName()."'.",
- ], 1);
-
- if (count($this->paramsArr) != 0) {
- foreach ($this->paramsArr as $name => $type) {
- $paramsComment[] = ' *';
- $paramsComment[] = " * @param $type \$$name One of the values which are used in 'where' condition.";
- }
- }
-
- $firstParamName = $colObj->isNull() ? 'newVal = null' : 'newVal';
- $paramsComment[] = ' *';
- $paramsComment[] = " * @param $phpType \$newVal The new value for the column.";
- $this->append($paramsComment, 1);
-
- if (strpos($phpType, '|null') !== false) {
- $phpType = '?'.substr($phpType,0, strlen($phpType) - strlen('|null'));
- }
- $this->append([
- " */",
-
- $this->f(self::toMethodName($key, 'update'), array_merge(
- $this->paramsArr,
- [$firstParamName => $phpType]
- ))
- ], 1);
- $this->append("\$this->table('".$t->getNormalName()."')->update([", 2);
- $this->append("'$key' => \$newVal", 4);
-
- if (count($this->whereArr) == 0) {
- $this->append("])->execute();", 3);
- $this->append("//TODO: Specify conditions for updating the value of the record '".$colObj->getNormalName()."'", 3);
- } else {
- $this->append("])", 3);
- $this->append($this->whereArr, 3);
- $this->append('->execute();', 3);
- }
-
- $this->append('}', 1);
- }
- private function writeDeleteRecord() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $this->append([
- "/**",
- " * Deletes a record from the table '".$t->getNormalName()."'.",
- " *",
- " * @param ".$this->getEntityName().' $entity An object that holds record information.',
- " */",
- $this->f('delete'.$this->getEntityName(), ['entity' => $this->getEntityName()]),
- ], 1);
- $this->append("\$this->table('".$t->getNormalName()."')", 2);
-
- if (count($this->paramsArr) != 0) {
- $this->append("->delete()", 4);
- $cols = [];
-
- foreach ($this->getUniqueColsKeys() as $key) {
- $cols[] = count($cols) == 0 ?
- "->where('$key', \$entity->".EntityMapper::mapToMethodName($key).'())'
- : "->andWhere('$key', \$entity->".EntityMapper::mapToMethodName($key).'())';
- }
- $this->append($cols, 4);
- $this->append("->execute();", 4);
- } else {
- $this->append("->delete();", 4);
- $this->append('//TODO: Specify delete record condition(s).', 3);
- }
- $this->append('}', 1);
- }
-
- private function writeGetRecord() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $this->append([
- "/**",
- " * Returns the information of a record from the table '".$t->getNormalName()."'.",
- " *",
- " * @return ".$this->getEntityName().'|null If a record with given information exist,',
- " * The method will return an object which holds all record information.",
- " * Other than that, null is returned.",
- " */",
- $this->f('get'.$this->getEntityName(), $this->paramsArr)
- ], 1);
- $this->append("\$mappedRecords = \$this->table('".$t->getNormalName()."')", 2);
- $this->append("->select()", 4);
-
- if (count($this->paramsArr) != 0) {
- $this->append($this->whereArr, 4);
- } else {
- $this->append('//TODO: Specify select condition for retrieving one record.', 4);
- }
- $this->append("->execute()", 4);
- $this->append("->map(function (array \$record) {", 4);
- $this->append("return ".$this->getEntityName().'::map($record);', 5);
- $this->append("});", 4);
- $this->append('if ($mappedRecords->getRowsCount() == 1) {', 2);
- $this->append('return $mappedRecords->getRows()[0];', 3);
- $this->append('}', 2);
- $this->append('}', 1);
- }
- private function writeGetRecords() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $this->append([
- "/**",
- " * Returns all the records from the table '".$t->getNormalName()."'.",
- " *",
- " * @param int \$pageNum The number of page to fetch. Default is 0.",
- " *",
- " * @param int \$pageSize Number of records per page. Default is 10.",
- " *",
- " * @return array An array that holds all table records as objects",
- " */",
- $this->f('get'.$this->getEntityName().'s', [
- 'pageNum = 0' => 'int',
- 'pageSize = 10' => 'int'
- ], 'array')
- ], 1);
- $this->append("return \$this->table('".$t->getNormalName()."')", 2);
- $this->append("->select()", 4);
- $this->append('->page($pageNum, $pageSize)', 4);
- $this->append('->orderBy(["id"])', 4);
- $this->append("->execute()", 4);
- $this->append("->map(function (array \$record) {", 4);
- $this->append("return ".$this->getEntityName().'::map($record);', 5);
- $this->append("})->toArray();", 4);
- $this->append('}', 1);
- }
- private function writeGetRecordsCount() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $this->append([
- "/**",
- " * Returns number of records on the table '".$t->getNormalName()."'.",
- " *",
- " * The main use of this method is to compute number of pages.",
- " *",
- " * @return int Number of records on the table '".$t->getNormalName()."'.",
- " */",
- $this->f('get'.$this->getEntityName().'sCount', [], 'int')
- ], 1);
- $this->append("return \$this->table('".$t->getNormalName()."')", 2);
- $this->append("->selectCount()", 4);
- $this->append("->execute()", 4);
- $this->append("->getRows()[0]['count'];", 4);
-
- $this->append('}', 1);
- }
- private function writeUpdateRecord() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $this->append([
- "/**",
- " * Updates a record on the table '".$t->getNormalName()."'.",
- " *",
- " * @param ".$this->getEntityName().' $entity An object that holds updated record information.',
- " */",
- $this->f('update'.$this->getEntityName(), ['entity' => $this->getEntityName()])
- ], 1);
- $this->append("\$this->table('".$t->getNormalName()."')", 2);
- $this->append("->update([", 3);
- $keys = $t->getColsKeys();
-
- if (count($this->paramsArr) != 0) {
- $updateCols = [];
- $whereCols = [];
- $uniqueCols = $this->getUniqueColsKeys();
-
- foreach ($uniqueCols as $key) {
- $whereCols[] = count($whereCols) == 0 ?
- "->where('$key', \$entity->".EntityMapper::mapToMethodName($key).'())'
- : "->andWhere('$key', \$entity->".EntityMapper::mapToMethodName($key).'())';
- }
-
- foreach ($keys as $key) {
- if (!in_array($key, $uniqueCols)) {
- $updateCols[] = "'$key' => \$entity->".EntityMapper::mapToMethodName($key).'(),';
- }
- }
- $this->append($updateCols, 4);
- $this->append('])', 3);
- $this->append($whereCols, 3);
- $this->append("->execute();", 3);
- } else {
- foreach ($keys as $key) {
- $updateCols[] = "'$key' => \$entity->".EntityMapper::mapToMethodName($key).'(),';
- }
- $this->append($updateCols, 4);
- $this->append(']);', 3);
- $this->append('//TODO: Specify update record condition(s).', 3);
- }
- $this->append('}', 1);
- }
- private function writeUpdateRecordMethods() {
- $t = $this->getTable();
-
- if ($t === null) {
- return;
- }
- $uniqueKeys = $this->getUniqueColsKeys();
-
- foreach ($t->getCols() as $key => $colObj) {
- if (!in_array($key, $uniqueKeys)) {
- $this->writeColUpdate($colObj, $key);
- }
- }
- }
-}
diff --git a/WebFiori/Framework/Writers/TableClassWriter.php b/WebFiori/Framework/Writers/TableClassWriter.php
deleted file mode 100644
index 0a7a945dd..000000000
--- a/WebFiori/Framework/Writers/TableClassWriter.php
+++ /dev/null
@@ -1,532 +0,0 @@
-
- * name: The name of the class that will be created. If not provided, the
- * string 'NewClass' is used.
- * namespace: The namespace that the class will belong to. If not provided,
- * the namespace 'WebFiori' is used.
- * path: The location at which the query will be created on. If not
- * provided, the constant ROOT_PATH is used.
- * entity-info: A sub associative array that contains information about the entity
- * at which the class is mapped to (if any). The array must have the following indices:
- *
- * - name: The name of the entity class that will be created.
- * - path: The location at which the entity class will be created on.
- * - namespace: The namespace at which the entity belongs to.
- * - implement-jsoni: A bollean which is set to true if the entity
- * class will implement the interface 'JsonI'.
- *
- *
- *
- *
- *
- * @since 1.0
- */
- public function __construct(?Table $tableObj = null) {
- parent::__construct('NewTable', APP_PATH.'Database', APP_DIR.'\\Database');
- $this->setSuffix('Table');
-
- if ($tableObj === null) {
- $this->setTableType('mysql');
-
- return;
- }
- $this->setTable($tableObj);
- }
- /**
- * Returns the name entity class will be created.
- *
- * @return string|null If the entity class information is set, the method will
- * return a string that represents the name of the entity class.
- *
- * Other than that, the method will return null.
- *
- * @since 1.0
- */
- public function getEntityName() {
- if ($this->entityMapper !== null) {
- return $this->entityMapper->getEntityName();
- }
- }
- /**
- * Returns the namespace that the associated entity class belongs to.
- *
- * @return string|null If the entity class information is set, the method will
- * return a string that represents the namespace that the entity belongs to.
- * Other than that, the method will return null.
- *
- * @since 1.0
- */
- public function getEntityNamespace() {
- if ($this->entityMapper !== null) {
- return $this->entityMapper->getNamespace();
- }
- }
-
- /**
- * Returns the location at which the entity class will be created on.
- *
- * @return string|null If the entity class information is set, the method will
- * return a string that represents the path that the entity will be created on.
- * Other than that, the method will return null.
- *
- * @since 1.0
- */
- public function getEntityPath() {
- if ($this->entityMapper !== null) {
- return $this->entityMapper->getPath();
- }
- }
- /**
- * Returns the table object which was associated with the writer.
- *
- * @return Table
- */
- public function getTable() : Table {
- return $this->tableObj;
- }
- /**
- * Sets the entity class info which mapps to a record in the table.
- *
- * @param string $className The name of the entity class.
- *
- * @param string $namespace The namespace at which the entity class will
- * belongs to.
- *
- * @param string $path The location at which the entity class will be
- * created at.
- *
- * @param bool $imlJsonI If set to true, the entity class will implement the
- * interface JsonI.
- */
- public function setEntityInfo(string $className, string $namespace, string $path, bool $imlJsonI) {
- $this->entityMapper = new EntityMapper($this->tableObj,
- $className,
- $path,
- $namespace);
- $this->entityMapper->setUseJsonI($imlJsonI);
- }
- /**
- * Sets the table that the writer will use in writing the table class.
- *
- * @param Table $table
- */
- public function setTable(Table $table) {
- $this->tableObj = $table;
-
- if ($table !== null) {
- $this->extractAndSetTableClassName();
- }
- }
- /**
- * Sets the type of database table engine.
- *
- * @param string $type The name of database server. It can have one of the
- * following values:
- *
- *
- */
- public function setTableType(string $type) {
- if ($type == 'mssql') {
- $this->tableObj = new MSSQLTable();
- } else if ($type == 'mysql') {
- $this->tableObj = new MySQLTable();
- }
- }
- /**
- * Write the query class.
- *
- * This method will first attempt to create the query class. If it was created,
- * it will create the entity class which is associated with it (if any
- * entity is associated).
- *
- * @since 1.0
- */
- public function writeClass() {
- $this->addAllUse();
- parent::writeClass();
-
- if ($this->entityMapper !== null) {
- $this->entityMapper->create();
- }
- }
-
- public function writeClassBody() {
- $this->writeConstructor();
- $this->append('}');
- }
-
- public function writeClassComment() {
- $this->append("/**\n"
- ." * A class which represents the database table '".$this->tableObj->getNormalName()."'.\n"
- ." * The table which is associated with this class will have the following columns:\n"
- ." * "
- );
-
- foreach ($this->tableObj->getCols() as $key => $colObj) {
- $this->append(" * - $key: Name in database: '".$colObj->getNormalName()."'. Data type: '".$colObj->getDatatype()."'.
");
- }
- $this->append(" *
\n */");
- }
-
- public function writeClassDeclaration() {
- if ($this->tableObj instanceof MySQLTable) {
- $this->append('class '.$this->getName().' extends MySQLTable {');
- } else if ($this->tableObj instanceof MSSQLTable) {
- $this->append('class '.$this->getName().' extends MSSQLTable {');
- }
- }
- private function addAllUse() {
- if ($this->tableObj instanceof MySQLTable) {
- $this->addUseStatement("WebFiori\Database\MySql\MySQLTable");
- } else if ($this->tableObj instanceof MSSQLTable) {
- $this->addUseStatement("WebFiori\Database\MsSql\MSSQLTable");
- }
- $this->addUseStatement(ColOption::class);
- $this->addUseStatement(DataType::class);
- $this->addFksUseTables();
- }
- private function addColsHelper() {
- $this->append('$this->addColumns([', 2);
-
- foreach ($this->tableObj->getCols() as $key => $colObj) {
- $this->appendColObj($key, $colObj);
- }
- $this->append(']);', 2);
- }
- private function addFKOption(Column $colObj) {
- $fks = $this->getTable()->getForeignKeys();
-
- foreach ($fks as $fk) {
- $sourceCols = array_values($fk->getOwnerCols());
-
- if (count($sourceCols) == 1 && $sourceCols[0]->getNormalName() == $colObj->getNormalName()) {
- $this->addFKOptionHelper($colObj, $fk);
- }
- }
- }
- private function addFKOptionHelper(Column $col, FK $fk) {
- $refTableNs = get_class($fk->getSource());
- $cName = $this->getNamespace().'\\'.$this->getName();
- $refTableClassName = '$this';
-
- if ($cName != $refTableNs) {
- $nsSplit = explode('\\', $refTableNs);
- $refTableClassName = 'new '.$nsSplit[count($nsSplit) - 1].'()';
- }
- $keyName = $fk->getKeyName();
- $sourceCol = array_keys($fk->getSourceCols())[0];
- $this->append("ColOption::FK => [", 4);
- $this->append("ColOption::FK_NAME => '".$keyName."',", 5);
- $this->append("ColOption::FK_TABLE => ".$refTableClassName.",", 5);
- $this->append("ColOption::FK_COL => '".$sourceCol."',", 5);
- $this->append("ColOption::FK_ON_UPDATE => ".$this->getFkCond($fk->getOnUpdate()).",", 5);
- $this->append("ColOption::FK_ON_DELETE => ".$this->getFkCond($fk->getOnDelete()).",", 5);
- $this->append("],", 4);
- }
- private function addFksHelper() {
- $fks = $this->tableObj->getForeignKeys();
-
- foreach ($fks as $fkObj) {
- if (count($fkObj->getSourceCols()) == 1) {
- continue;
- }
- $refTableNs = get_class($fkObj->getSource());
- $cName = $this->getNamespace().'\\'.$this->getName();
- $refTableClassName = '$this';
-
- if ($cName != $refTableNs) {
- $nsSplit = explode('\\', $refTableNs);
- $refTableClassName = 'new '.$nsSplit[count($nsSplit) - 1].'()';
- }
-
- $this->append('$this->addReference('.$refTableClassName.', [', 2);
- $ownerCols = array_keys($fkObj->getOwnerCols());
- $sourceCols = array_keys($fkObj->getSourceCols());
-
- for ($x = 0 ; $x < count($ownerCols) ; $x ++) {
- $this->append("'$ownerCols[$x]' => '$sourceCols[$x]',", 3);
- }
- $this->append("], '".$fkObj->getKeyName()."', '".$fkObj->getOnUpdate()."', '".$fkObj->getOnDelete()."');", 2);
- }
- }
- private function addFksUseTables() {
- if ($this->tableObj !== null) {
- $fks = $this->tableObj->getForeignKeys();
-
- if (count($fks) != 0) {
- $this->addUseStatement(FK::class);
- }
- $addedRefs = [];
-
- foreach ($fks as $fkObj) {
- $refTableNs = get_class($fkObj->getSource());
-
- if (!in_array($refTableNs, $addedRefs)) {
- $this->addUseStatement($refTableNs);
- $addedRefs[] = $refTableNs;
- }
- }
- }
- }
- /**
- *
- * @param MySQLColumn $colObj
- */
- private function appendColObj($key, $colObj) {
- $dataType = $colObj->getDatatype();
- $this->append("'$key' => [", 3);
- $this->append("ColOption::TYPE => ".$this->getType($colObj->getDatatype()).",", 4);
-
- if (($dataType == 'int' && $colObj instanceof MySQLColumn)
- || $dataType == 'varchar'
- || $dataType == 'decimal'
- || $dataType == 'float'
- || $dataType == 'double'
- || $dataType == 'binary'
- || $dataType == 'varbinary'
- || $dataType == 'char'
- || $dataType == 'nchar'
- || $dataType == 'nvarchar') {
- $this->append("ColOption::SIZE => '".$colObj->getSize()."',", 4);
-
- if ($dataType == 'decimal') {
- $this->append("ColOption::SCALE => '".$colObj->getScale()."',", 4);
- }
- }
-
- if ($colObj instanceof MSSQLColumn && $colObj->isIdentity()) {
- $this->append("ColOption::IDENTITY => true,", 4);
- }
-
- if ($colObj->isPrimary()) {
- $this->append("ColOption::PRIMARY => true,", 4);
-
- if ($colObj instanceof MySQLColumn && $colObj->isAutoInc()) {
- $this->append("ColOption::AUTO_INCREMENT => true,", 4);
- }
- }
-
- if ($colObj->isUnique()) {
- $this->append("ColOption::UNIQUE => true,", 4);
- }
-
- if ($colObj->getDefault() !== null) {
- $defaultVal = "ColOption::DEFAULT => '".$colObj->getDefault()."',";
-
- if (in_array($dataType, Column::BOOL_TYPES)) {
- $defaultVal = $colObj->getDefault() === true ? "ColOption::DEFAULT => true," : "ColOption::DEFAULT => false,";
- } else if ($dataType == 'int' || $dataType == 'bigint' || $dataType == 'decimal' || $dataType == 'money') {
- $defaultVal = "ColOption::DEFAULT => ".$colObj->getDefault().",";
- }
- $this->append($defaultVal, 4);
- }
-
- if ($colObj->isNull()) {
- $this->append("ColOption::NULL => true,", 4);
- }
-
- if ($colObj->getComment() !== null) {
- $this->append("ColOption::COMMENT => '".$colObj->getComment()."',", 4);
- }
- $this->addFKOption($colObj);
- $this->append("],", 3);
- }
- /**
- * Extract and return the name of table class based on associated table object.
- *
- */
- private function extractAndSetTableClassName() {
- $clazz = get_class($this->getTable());
-
- $split = explode('\\', $clazz);
- $count = count($split);
-
- if ($count > 1) {
- $this->setClassName($split[$count - 1]);
- array_pop($split);
- $this->setNamespace(implode('\\', $split));
- } else {
- $this->setClassName($split[0]);
- }
- }
- private function getFkCond(string $txt) {
- switch ($txt) {
- case 'cascade' :{
- return 'FK::CASCADE';
- }
- case 'no action' :{
- return 'FK::NO_ACTION';
- }
- case 'restrict' :{
- return 'FK::RESTRICT';
- }
- case 'set default' :{
- return 'FK::SET_DEFAULT';
- }
- case 'set null' :{
- return 'FK::SET_NULL';
- }
- }
- }
- private function getType(string $dataType) {
- switch ($dataType) {
- case 'bigint' : {
- return 'DataType::BIGINT';
- }
- case 'binary' : {
- return 'DataType::BINARY';
- }
- case 'bit' : {
- return 'DataType::BIT';
- }
- case 'blob' : {
- return 'DataType::BLOB';
- }
- case 'longblob' : {
- return 'DataType::BLOB_LONG';
- }
- case 'mediumblob' : {
- return 'DataType::BLOB_MEDIUM';
- }
- case 'tinyblob' : {
- return 'DataType::BLOB_TINY';
- }
- case 'bool' : {
- return 'DataType::BOOL';
- }
- case 'boolean' : {
- return 'DataType::BOOL';
- }
- case 'char' : {
- return 'DataType::CHAR';
- }
- case 'date' : {
- return 'DataType::DATE';
- }
- case 'datetime' : {
- return 'DataType::DATETIME';
- }
- case 'datetime2' : {
- return 'DataType::DATETIME2';
- }
- case 'decimal' : {
- return 'DataType::DECIMAL';
- }
- case 'double' : {
- return 'DataType::DOUBLE';
- }
- case 'float' : {
- return 'DataType::FLOAT';
- }
- case 'int' : {
- return 'DataType::INT';
- }
- case 'money' : {
- return 'DataType::MONEY';
- }
- case 'nchar' : {
- return 'DataType::NCHAR';
- }
- case 'nvarchar' : {
- return 'DataType::NVARCHAR';
- }
- case 'text' : {
- return 'DataType::TEXT';
- }
- case 'medumtext' : {
- return 'DataType::TEXT_MEDIUM';
- }
- case 'time' : {
- return 'DataType::TIME';
- }
- case 'timestamp' : {
- return 'DataType::TIMESTAMP';
- }
- case 'varbinary' : {
- return 'DataType::VARBINARY';
- }
- case 'varchar' : {
- return 'DataType::VARCHAR';
- }
- default : {
- return "'mixed'";
- }
- }
- }
- protected function writeConstructor(array $params = [],
- $body = '',
- string $description = 'Creates new instance of the class.',
- int $indent = 1) {
- $this->append([
- "/**",
- " * Creates new instance of the class.",
- " */",
- $this->f('__construct'),
- ], 1);
- $this->append('parent::__construct(\''.$this->tableObj->getNormalName().'\');', 2);
-
- if ($this->tableObj->getComment() !== null) {
- $this->append('$this->setComment(\''.$this->tableObj->getComment().'\');', 2);
- }
- $this->addColsHelper();
- $this->addFksHelper();
- $this->append('}', 1);
- }
-}
diff --git a/WebFiori/Framework/Writers/WebServiceWriter.php b/WebFiori/Framework/Writers/WebServiceWriter.php
deleted file mode 100644
index c01238b92..000000000
--- a/WebFiori/Framework/Writers/WebServiceWriter.php
+++ /dev/null
@@ -1,351 +0,0 @@
-
- * name: The name of the class that will be created. If not provided, the
- * string 'NewClass' is used.
- * namespace: The namespace that the class will belong to. If not provided,
- * the namespace 'WebFiori' is used.
- * path: The location at which the query will be created on. If not
- * provided, the constant ROOT_PATH is used.
- *
- */
- public function __construct(?AbstractWebService $webServicesObj = null) {
- parent::__construct('NewWebService', APP_PATH.'Apis', APP_DIR.'\\Apis');
-
- $this->setSuffix('Service');
- $this->addUseStatement(AbstractWebService::class);
- $this->addUseStatement(ParamType::class);
- $this->addUseStatement(ParamOption::class);
- $this->addUseStatement(RequestMethod::class);
- $this->servicesObj = new ServiceHolder();
-
- if ($webServicesObj instanceof AbstractWebService) {
- $this->servicesObj = $webServicesObj;
- }
- $this->processCode = [];
- }
- public function addProcessCode($lineOrLines, $tab = 2) {
- $arrToAdd = [
- 'tab-size' => $tab,
- 'lines' => []
- ];
-
- if (gettype($lineOrLines) == 'array') {
- foreach ($lineOrLines as $l) {
- $arrToAdd['lines'][] = $l;
- }
- } else {
- $arrToAdd['lines'][] = $lineOrLines;
- }
- $this->processCode[] = $arrToAdd;
- }
- /**
- * Adds new request method.
- *
- * The value that will be passed to this method can be any string
- * that represents HTTP request method (e.g. 'get', 'post', 'options' ...). It
- * can be in upper case or lower case.
- *
- * @param string $meth The request method.
- *
- */
- public function addRequestMethod($meth) {
- $this->servicesObj->addRequestMethod($meth);
- }
- /**
- * Adds new request parameter.
- *
- * The parameter will only be added if no parameter which has the same
- * name as the given one is added before.
- *
- * @param RequestParameter|array $param The parameter that will be added. It
- * can be an object of type 'RequestParameter' or an associative array of
- * options. The array can have the following indices:
- *
- * - name: The name of the parameter. It must be provided.
- * - type: The datatype of the parameter. If not provided, 'string' is used.
- * - optional: A boolean. If set to true, it means the parameter is
- * optional. If not provided, 'false' is used.
- * - min: Minimum value of the parameter. Applicable only for
- * numeric types.
- * - max: Maximum value of the parameter. Applicable only for
- * numeric types.
- * - allow-empty: A boolean. If the type of the parameter is string or string-like
- * type and this is set to true, then empty strings will be allowed. If
- * not provided, 'false' is used.
- * - custom-filter: A PHP function that can be used to filter the
- * parameter even further
- * - default: An optional default value to use if the parameter is
- * not provided and is optional.
- * - description: The description of the attribute.
- *
- *
- * @return boolean If the given request parameter is added, the method will
- * return true. If it was not added for any reason, the method will return
- * false.
- *
- * @since 1.0
- */
- public function addRequestParam($options) : bool {
- return $this->servicesObj->addParameter($options);
- }
-
- public function writeClassBody() {
- $this->writeConstructor();
- $this->implementMethods();
- $this->append('}');
- }
-
- public function writeClassComment() {
- $this->append([
- "",
- '',
- "/**",
- " * A class that contains the implementation of the web service '".$this->servicesObj->getName()."'."
- ]);
- $this->writeServiceDoc($this->servicesObj);
- $this->append(" */");
- }
-
- public function writeClassDeclaration() {
- $this->append('class '.$this->getName().' extends AbstractWebService {');
- }
- /**
- *
- * @param RequestParameter $param
- */
- private function appendParam($param) {
- $this->append("'".$param->getName()."' => [", 3);
-
- $this->append("ParamOption::TYPE => ".$this->getType($param->getType()).",", 4);
-
- if ($param->isOptional()) {
- $this->append("ParamOption::OPTIONAL => true,", 4);
- }
-
- if ($param->getDefault() !== null) {
- $toAppend = "ParamOption::DEFAULT => ".$param->getDefault().",";
-
- if (($param->getType() == ParamType::STRING || $param->getType() == ParamType::URL || $param->getType() == ParamType::EMAIL) && strlen($param->getDefault()) > 0) {
- $toAppend = "ParamOption::DEFAULT => '".$param->getDefault()."',";
- } else if ($param->getType() == ParamType::BOOL) {
- $toAppend = $param->getDefault() === true ? "ParamOption::DEFAULT => true," : "ParamOption::DEFAULT => false,";
- }
- $this->append($toAppend, 4);
- }
-
- if (($param->getType() == ParamType::STRING || $param->getType() == ParamType::URL || $param->getType() == ParamType::EMAIL)) {
- if ($param->isEmptyStringAllowed()) {
- $this->append("ParamOption::EMPTY => true,", 4);
- }
-
- if ($param->getMinLength() !== null) {
- $this->append("ParamOption::MIN_LENGTH => ".$param->getMinLength().",", 4);
- }
-
- if ($param->getMaxLength() !== null) {
- $this->append("ParamOption::MAX_LENGTH => ".$param->getMaxLength().",", 4);
- }
- }
-
- if ($param->getType() == ParamType::INT || $param->getType() == ParamType::DOUBLE) {
-
- if ($param->getMinValue() !== null && $param->getMinValue() != -1e50) {
- $this->append("ParamOption::MIN => ".$param->getMinValue().",", 4);
- }
-
- if ($param->getMaxValue() !== null && $param->getMaxValue() != PHP_INT_MAX && $param->getMaxValue() != 1e50) {
- $this->append("ParamOption::MAX => ".$param->getMaxValue().",", 4);
- }
- }
-
- if ($param->getDescription() !== null) {
- $this->append("ParamOption::DESCRIPTION => '".str_replace('\'', '\\\'', $param->getDescription())."',", 4);
- }
- $this->append('],', 3);
- }
- private function appendParams($paramsArray) {
- if (count($paramsArray) !== 0) {
- $this->append('$this->addParameters([', 2);
-
- foreach ($paramsArray as $paramObj) {
- $this->appendParam($paramObj);
- }
- $this->append(']);', 2);
- }
- }
- private function getMethod($method) {
- switch ($method) {
- case RequestMethod::CONNECT:{
- return "RequestMethod::CONNECT";
- }
- case RequestMethod::DELETE:{
- return "RequestMethod::DELETE";
- }
- case RequestMethod::GET:{
- return "RequestMethod::GET";
- }
- case RequestMethod::HEAD:{
- return "RequestMethod::HEAD";
- }
- case RequestMethod::OPTIONS:{
- return "RequestMethod::OPTIONS";
- }
- case RequestMethod::PATCH:{
- return "RequestMethod::PATCH";
- }
- case RequestMethod::POST:{
- return "RequestMethod::POST";
- }
- case RequestMethod::PUT:{
- return "RequestMethod::PUT";
- }
- case RequestMethod::TRACE:{
- return "RequestMethod::TRACE";
- }
- }
- }
- private function getType(string $type) {
- switch ($type) {
- case 'int': {
- return 'ParamType::INT';
- }
- case 'integer': {
- return 'ParamType::INT';
- }
- case 'string': {
- return 'ParamType::STRING';
- }
- case 'array': {
- return 'ParamType::ARR';
- }
- case 'bool': {
- return 'ParamType::BOOL';
- }
- case 'boolean': {
- return 'ParamType::BOOL';
- }
- case 'double': {
- return 'ParamType::DOUBLE';
- }
- case 'email': {
- return 'ParamType::EMAIL';
- }
- case 'json-obj': {
- return 'ParamType::JSON_OBJ';
- }
- case 'url': {
- return 'ParamType::URL';
- }
- }
- }
- private function implementMethods() {
- $name = $this->servicesObj->getName();
- $this->append([
- "/**",
- " * Checks if the client is authorized to call a service or not.",
- " *",
- " * @return boolean If the client is authorized, the method will return true.",
- " */",
- $this->f('isAuthorized', [], 'bool'),
- ], 1);
- $this->append([
- '// TODO: Check if the client is authorized to call the service \''.$name.'\'.',
- '// You can ignore this method or remove it.',
- '//$authHeader = $this->getAuthHeader();',
- '//$authType = $authHeader[\'type\'];',
- '//$token = $authHeader[\'credentials\'];',
- 'return true;'
- ], 2);
- $this->append('}', 1);
-
- $this->append([
- "/**",
- " * Process the request.",
- " */",
- $this->f('processRequest'),
- ], 1);
-
- if (count($this->processCode) == 0) {
- $this->append('// TODO: process the request for the service \''.$name.'\'.', 2);
- $this->append('$this->getManager()->serviceNotImplemented();', 2);
- } else {
- foreach ($this->processCode as $arr) {
- $this->append($arr['lines'], $arr['tab-size']);
- }
- }
- $this->append('}', 1);
- }
- protected function writeConstructor(array $params = [],
- $body = '',
- string $description = 'Creates new instance of the class.',
- int $indent = 1) {
- $this->append([
- "/**",
- " * Creates new instance of the class.",
- " */",
- $this->f('__construct'),
- ], 1);
- $this->append('parent::__construct(\''.$this->servicesObj->getName().'\');', 2);
- $this->append('$this->setDescription(\''.str_replace("'", "\\'", $this->servicesObj->getDescription()).'\');', 2);
- $this->append('$this->setRequestMethods([', 2);
-
- foreach ($this->servicesObj->getRequestMethods() as $method) {
- $this->append($this->getMethod($method).',', 3);
- }
- $this->append(']);', 2);
- $this->appendParams($this->servicesObj->getParameters());
- $this->append('}', 1);
- }
- private function writeServiceDoc($service) {
- $docArr = [];
-
- if (count($service->getParameters()) != 0) {
- $docArr[] = " * This service has the following parameters:";
- $docArr[] = ' * ';
-
- foreach ($service->getParameters() as $param) {
- $docArr[] = ' * - '.$param->getName().': Data type: '.$param->getType().'.
';
- }
- $docArr[] = ' *
';
- $this->append($docArr);
- }
- }
-}
From c603b74ccd8db008f3222dfb64c415a59bbd4a70 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 00:09:05 +0300
Subject: [PATCH 38/88] test: Removed Deprecated Test Cases
---
.../Tests/Cli/DBClassWritterTest.php | 107 --------
.../Tests/Writers/TableWritterTest.php | 237 ------------------
.../Writers/WebServiceTestCaseWriterTest.php | 41 ---
.../Tests/Writers/WebServiceWritterTest.php | 141 -----------
4 files changed, 526 deletions(-)
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/DBClassWritterTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Writers/TableWritterTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Writers/WebServiceTestCaseWriterTest.php
delete mode 100644 tests/WebFiori/Framework/Tests/Writers/WebServiceWritterTest.php
diff --git a/tests/WebFiori/Framework/Tests/Cli/DBClassWritterTest.php b/tests/WebFiori/Framework/Tests/Cli/DBClassWritterTest.php
deleted file mode 100644
index f34ade04b..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/DBClassWritterTest.php
+++ /dev/null
@@ -1,107 +0,0 @@
-getEntityMapper();
- $mapper->setEntityName('CoolUser');
- $mapper->setNamespace('WebFiori\\Entity');
- $writter = new DBClassWriter('UserDBClass', 'WebFiori\\Db', $table);
- $writter->writeClass();
-
- // Check if file was written and require it
- $filePath = $writter->getPath() . DS . $writter->getName() . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($writter->getName(true)));
- $this->removeClass($writter->getName(true));
- }
-
- /**
- * @test
- */
- public function test01() {
- $table = new EmployeeInfoTable();
- $mapper = $table->getEntityMapper();
- $mapper->setEntityName('Employee');
- $mapper->setNamespace('WebFiori\\Entity');
- $writter = new DBClassWriter('EmployeeDB', 'WebFiori\\Db', $table);
- $writter->writeClass();
-
- // Check if file was written and require it
- $filePath = $writter->getPath() . DS . $writter->getName() . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($writter->getName(true)));
- $this->removeClass($writter->getName(true));
- }
-
- /**
- * @test
- */
- public function test02() {
- $table = new PositionInfoTable();
- $mapper = $table->getEntityMapper();
- $mapper->setEntityName('Position');
- $mapper->setNamespace('WebFiori\\Entity');
- $writter = new DBClassWriter('PositionDB', 'WebFiori\\Db', $table);
- $writter->writeClass();
-
- // Check if file was written and require it
- $filePath = $writter->getPath() . DS . $writter->getName() . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($writter->getName(true)));
- $this->removeClass($writter->getName(true));
- }
-
- /**
- * @test
- */
- public function test03() {
- $table = new PositionInfoTable();
- $writter = new DBClassWriter('PositionDB2', 'WebFiori\\Db', $table);
- $writter->setConnection(' ');
- $this->assertNull($writter->getConnectionName());
- $writter->setConnection('ok-connection');
- $this->assertEquals('ok-connection', $writter->getConnectionName());
- $writter->includeColumnsUpdate();
- $writter->writeClass();
-
- // Check if file was written and require it
- $filePath = $writter->getPath() . DS . $writter->getName() . '.php';
- $this->assertTrue(file_exists($filePath), "Class file was not created: $filePath");
- require_once $filePath;
- $this->assertTrue(class_exists($writter->getName(true)));
- $this->removeClass($writter->getName(true));
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Writers/TableWritterTest.php b/tests/WebFiori/Framework/Tests/Writers/TableWritterTest.php
deleted file mode 100644
index 1f4d76278..000000000
--- a/tests/WebFiori/Framework/Tests/Writers/TableWritterTest.php
+++ /dev/null
@@ -1,237 +0,0 @@
-assertEquals('NewTable', $writter->getName());
- $this->assertEquals('App\\Database', $writter->getNamespace());
- $this->assertEquals('Table', $writter->getSuffix());
- $this->assertEquals([
-
- ], $writter->getUseStatements());
- $this->assertNull($writter->getEntityName());
- $this->assertNull($writter->getEntityNamespace());
- $this->assertNull($writter->getEntityPath());
- $this->assertTrue($writter->getTable() instanceof MySQLTable);
- $this->assertFalse($writter->getTable() instanceof MSSQLTable);
- }
- /**
- * @test
- */
- public function test01() {
- $writter = new TableClassWriter();
- $writter->setClassName('CoolT');
- $writter->setEntityInfo('MyEntity', 'App\\Entity', ROOT_PATH.DS.APP_DIR.DS.'Entity', true);
- $this->assertEquals('CoolTTable', $writter->getName());
- $this->assertEquals('App\\Database', $writter->getNamespace());
- $this->assertEquals('Table', $writter->getSuffix());
- $this->assertEquals([
-
- ], $writter->getUseStatements());
- $this->assertEquals('MyEntity', $writter->getEntityName());
- $this->assertEquals('App\\Entity', $writter->getEntityNamespace());
- $this->assertEquals(ROOT_PATH.DS.APP_DIR.DS.'Entity', $writter->getEntityPath());
- $this->assertTrue($writter->getTable() instanceof MySQLTable);
- $this->assertFalse($writter->getTable() instanceof MSSQLTable);
- $writter->writeClass();
- $clazz = $writter->getName(true);
- $this->assertTrue(class_exists($clazz));
- $writter->removeClass();
- $clazzObj = new $clazz();
- $this->assertTrue($clazzObj instanceof MySQLTable);
- $this->assertEquals('`new_table`', $clazzObj->getName());
- $this->assertEquals(0, $clazzObj->getColsCount());
- $this->removeClass('App\\Entity\\MyEntity');
- }
- /**
- * @test
- */
- public function test02() {
- $writter = new TableClassWriter();
- $writter->setClassName('CoolT2Table');
- $writter->setTableType('mssql');
- $writter->setEntityInfo('MyEntity', 'App\\Entity', ROOT_PATH.DS.APP_DIR.DS.'Entity', true);
- $this->assertEquals('CoolT2Table', $writter->getName());
-
- $this->assertFalse($writter->getTable() instanceof MySQLTable);
- $this->assertTrue($writter->getTable() instanceof MSSQLTable);
- $writter->writeClass();
- $clazz = $writter->getName(true);
- $this->assertTrue(class_exists($clazz));
- $writter->removeClass();
- $clazzObj = new $clazz();
- $this->assertTrue($clazzObj instanceof MSSQLTable);
- $this->assertEquals('[new_table]', $clazzObj->getName());
- $this->assertEquals(0, $clazzObj->getColsCount());
- $this->removeClass('App\\Entity\\MyEntity');
- }
- /**
- * @test
- */
- public function test03() {
- $writter = new TableClassWriter();
- $writter->setClassName('CoolT3Table');
- $writter->setTableType('mssql');
-
- $writter->getTable()->addColumns([
- 'col-1' => [],
- 'col-2' => [],
- 'col-3' => []
- ]);
- $writter->getTable()->setName('super');
- $writter->writeClass();
- $clazz = $writter->getName(true);
- $this->assertTrue(class_exists($clazz));
- $writter->removeClass();
- $clazzObj = new $clazz();
- $this->assertTrue($clazzObj instanceof MSSQLTable);
- $this->assertEquals('[super]', $clazzObj->getName());
- $this->assertEquals(3, $clazzObj->getColsCount());
- $col00 = $clazzObj->getColByKey('col-1');
- $this->assertEquals('mixed', $col00->getDatatype());
- $this->assertEquals(1, $col00->getSize());
- $this->assertNull($col00->getDefault());
- $this->assertFalse($col00->isNull());
- $this->assertFalse($col00->isPrimary());
- $this->assertFalse($col00->isUnique());
- }
- /**
- * @test
- */
- public function test04() {
- $writter = new TableClassWriter();
- $writter->setClassName('CoolT4Table');
-
- $writter->getTable()->addColumns([
- 'col-1' => [],
- 'col-2' => [],
- 'col-3' => []
- ]);
- $writter->getTable()->setName('super');
- $writter->writeClass();
- $clazz = $writter->getName(true);
- $this->assertTrue(class_exists($clazz));
- $writter->removeClass();
- $clazzObj = new $clazz();
- $this->assertTrue($clazzObj instanceof MySQLTable);
- $this->assertEquals('`super`', $clazzObj->getName());
- $this->assertEquals(3, $clazzObj->getColsCount());
- $col00 = $clazzObj->getColByKey('col-1');
- $this->assertEquals('mixed', $col00->getDatatype());
- $this->assertEquals(1, $col00->getSize());
- $this->assertNull($col00->getDefault());
- $this->assertFalse($col00->isNull());
- $this->assertFalse($col00->isPrimary());
- $this->assertFalse($col00->isUnique());
- }
- /**
- * @test
- */
- public function test05() {
- $writter = new TableClassWriter();
- $writter->setClassName('CoolT5Table');
- $writter->getTable()->setComment('The table that holds user info.');
- $writter->getTable()->addColumns([
- 'col-1' => [
- 'type' => 'int',
- 'primary' => true,
- 'comment' => 'The unique identifier of the table.',
- 'auto-inc' => true
- ],
- 'col-2' => [
- 'type' => 'varchar',
- 'size' => 300,
- 'is-null' => true,
- 'default' => 'Hello World!'
- ],
- 'col-3' => [
- 'type' => 'timestamp',
- 'default' => 'current_timestamp'
- ],
- 'col-4' => [
- 'type' => 'bool',
- 'default' => true
- ],
- 'col-5' => [
- 'type' => 'bool',
- 'default' => false
- ],
- 'col-6' => [
- 'type' => 'decimal',
- 'size' => 10,
- 'scale' => '4',
- 'default' => true
- ]
- ]);
- $writter->getTable()->setName('super');
- $writter->writeClass();
- $clazz = $writter->getName(true);
- $this->assertTrue(class_exists($clazz));
- $writter->removeClass();
- $clazzObj = new $clazz();
- $this->assertTrue($clazzObj instanceof MySQLTable);
- $this->assertEquals('`super`', $clazzObj->getName());
- $this->assertEquals(6, $clazzObj->getColsCount());
- $this->assertEquals('The table that holds user info.', $clazzObj->getComment());
-
- $col00 = $clazzObj->getColByKey('col-1');
- $this->assertTrue($col00 instanceof MySQLColumn);
- $this->assertEquals('int', $col00->getDatatype());
- $this->assertEquals(1, $col00->getSize());
- $this->assertNull($col00->getDefault());
- $this->assertTrue($col00->isPrimary());
- $this->assertFalse($col00->isNull());
- $this->assertTrue($col00->isUnique());
- $this->assertTrue($col00->isAutoInc());
- $this->assertFalse($col00->isAutoUpdate());
- $this->assertEquals('The unique identifier of the table.', $col00->getComment());
-
-
- $col01 = $clazzObj->getColByKey('col-2');
- $this->assertTrue($col01 instanceof MySQLColumn);
- $this->assertEquals('varchar', $col01->getDatatype());
- $this->assertEquals(300, $col01->getSize());
- $this->assertEquals('Hello World!', $col01->getDefault());
- $this->assertFalse($col01->isPrimary());
- $this->assertTrue($col01->isNull());
- $this->assertFalse($col01->isUnique());
- $this->assertFalse($col01->isAutoInc());
- $this->assertFalse($col01->isAutoUpdate());
- $this->assertNull($col01->getComment());
-
- $col04 = $clazzObj->getColByKey('col-4');
- $this->assertTrue($col04 instanceof MySQLColumn);
- $this->assertEquals('bool', $col04->getDatatype());
- $this->assertFalse($col04->isNull());
- $this->assertFalse($col04->isUnique());
- $this->assertTrue($col04->getDefault());
- $this->assertNull($col04->getComment());
-
- $col05 = $clazzObj->getColByKey('col-5');
- $this->assertTrue($col05 instanceof MySQLColumn);
- $this->assertEquals('bool', $col05->getDatatype());
- $this->assertFalse($col05->isNull());
- $this->assertFalse($col05->isUnique());
- $this->assertFalse($col05->getDefault());
- $this->assertNull($col05->getComment());
-
- $col06 = $clazzObj->getColByKey('col-6');
- $this->assertTrue($col06 instanceof MySQLColumn);
- $this->assertEquals('decimal', $col06->getDatatype());
- $this->assertEquals(10, $col06->getSize());
- $this->assertEquals(4, $col06->getScale());
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Writers/WebServiceTestCaseWriterTest.php b/tests/WebFiori/Framework/Tests/Writers/WebServiceTestCaseWriterTest.php
deleted file mode 100644
index c7c75435a..000000000
--- a/tests/WebFiori/Framework/Tests/Writers/WebServiceTestCaseWriterTest.php
+++ /dev/null
@@ -1,41 +0,0 @@
-assertEquals('tests\\Apis\\WebServiceTest', $w->getName(true));
- $this->assertEquals(9, $w->getPhpUnitVersion());
- $this->assertEquals(ROOT_PATH.DS.'tests'.DS.'Apis'.DS.'WebServiceTest.php', $w->getAbsolutePath());
- $w->writeClass();
- $this->assertTrue(class_exists('\\'.$w->getName(true)));
- unlink($w->getAbsolutePath());
- }
- /**
- * @test
- */
- public function test01() {
- $w = new APITestCaseWriter(new TasksServicesManager(), new ForceTaskExecutionService());
- $w->setClassName('Cool');
- $w->setNamespace('\\tests\\cool');
- $w->setPath(ROOT_PATH.DS.'tests'.DS.'cool');
- $this->assertEquals('tests\\cool\\CoolTest', $w->getName(true));
- $w->writeClass();
- $this->assertEquals(ROOT_PATH.DS.'tests'.DS.'cool'.DS.'CoolTest.php', $w->getAbsolutePath());
- $this->assertTrue(file_exists($w->getAbsolutePath()));
- require_once $w->getAbsolutePath();
- $this->assertTrue(class_exists('\\'.$w->getName(true)));
- unlink($w->getAbsolutePath());
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Writers/WebServiceWritterTest.php b/tests/WebFiori/Framework/Tests/Writers/WebServiceWritterTest.php
deleted file mode 100644
index 139c7ef0d..000000000
--- a/tests/WebFiori/Framework/Tests/Writers/WebServiceWritterTest.php
+++ /dev/null
@@ -1,141 +0,0 @@
-assertEquals('NewWebService', $writter->getName());
- $this->assertEquals('App\\Apis', $writter->getNamespace());
- $this->assertEquals('Service', $writter->getSuffix());
- $this->assertEquals([
- "WebFiori\Http\AbstractWebService",
- "WebFiori\Http\ParamType",
- "WebFiori\Http\ParamOption",
- "WebFiori\\Http\\RequestMethod"
- ], $writter->getUseStatements());
- }
- /**
- * @test
- */
- public function test01() {
- $writter = new WebServiceWriter();
- $writter->setClassName('SuperService');
- $this->assertEquals('SuperService', $writter->getName());
- $this->assertEquals('App\\Apis', $writter->getNamespace());
- $this->assertEquals('Service', $writter->getSuffix());
- $this->assertEquals([
- "WebFiori\Http\AbstractWebService",
- "WebFiori\Http\ParamType",
- "WebFiori\Http\ParamOption",
- "WebFiori\\Http\\RequestMethod"
- ], $writter->getUseStatements());
- $writter->addRequestParam([
- 'name' => 'param-1',
- 'type' => 'boolean'
- ]);
- $writter->addRequestMethod('get');
- $writter->writeClass();
- $clazz = '\\'.$writter->getNamespace().'\\'.$writter->getName();
- $this->assertTrue(class_exists($clazz));
- $clazzObj = new $clazz();
- $this->assertTrue($clazzObj instanceof AbstractWebService);
- $this->assertEquals(1, count($clazzObj->getParameters()));
- $writter->removeClass();
- }
- /**
- * @test
- */
- public function test02() {
- $writter = new WebServiceWriter();
- $writter->setClassName('Super2Service');
- $this->assertEquals('Super2Service', $writter->getName());
- $this->assertEquals('App\\Apis', $writter->getNamespace());
- $this->assertEquals('Service', $writter->getSuffix());
- $this->assertEquals([
- "WebFiori\Http\AbstractWebService",
- "WebFiori\Http\ParamType",
- "WebFiori\Http\ParamOption",
- "WebFiori\\Http\\RequestMethod"
- ], $writter->getUseStatements());
- $writter->addRequestParam([
- 'name' => 'param-1',
- 'type' => 'boolean'
- ]);
- $writter->addRequestParam([
- 'name' => 'param-2',
- 'type' => 'boolean',
- 'default' => false,
- 'description' => 'A bool.'
- ]);
- $writter->addRequestParam([
- 'name' => 'param-3',
- 'type' => 'string',
- 'optional' => true,
- 'default' => 'Ok',
- 'allow-empty' => true
- ]);
- $writter->addRequestParam([
- 'name' => 'param-4',
- 'type' => 'string',
- 'allow-empty' => true
- ]);
- $writter->addRequestParam([
- 'name' => 'param-5',
- 'type' => 'boolean',
- 'default' => true,
- 'description' => 'A second bool.'
- ]);
- $writter->addRequestParam([
- 'name' => 'param-6',
- 'type' => 'integer',
- 'default' => 66,
- 'description' => 'A number.',
- 'optional' => true
- ]);
- $writter->addRequestMethod('get');
- $writter->writeClass();
- $clazz = $writter->getName(true);
- $this->assertTrue(class_exists($clazz));
- $clazzObj = new $clazz();
- $writter->removeClass();
- $this->assertTrue($clazzObj instanceof AbstractWebService);
- $this->assertEquals(6, count($clazzObj->getParameters()));
-
- $param1 = $clazzObj->getParameterByName('param-1');
-
- $this->assertEquals('boolean', $param1->getType());
- $this->assertNull($param1->getDefault());
- $this->assertNull($param1->getDescription());
- $this->assertFalse($param1->isOptional());
- $this->assertFalse($param1->isEmptyStringAllowed());
-
- $param2 = $clazzObj->getParameterByName('param-2');
- $this->assertEquals('boolean', $param2->getType());
- $this->assertFalse($param2->getDefault());
- $this->assertEquals('A bool.', $param2->getDescription());
- $this->assertFalse($param2->isOptional());
- $this->assertFalse($param2->isEmptyStringAllowed());
-
- $param3 = $clazzObj->getParameterByName('param-3');
- $this->assertEquals('string', $param3->getType());
- $this->assertEquals('Ok', $param3->getDefault());
- $this->assertNull($param3->getDescription());
- $this->assertTrue($param3->isOptional());
- $this->assertTrue($param3->isEmptyStringAllowed());
-
- $param3 = $clazzObj->getParameterByName('param-6');
- $this->assertEquals('integer', $param3->getType());
- $this->assertEquals(66, $param3->getDefault());
- $this->assertEquals('A number.', $param3->getDescription());
- $this->assertTrue($param3->isOptional());
- }
-}
From e1a6e810a3ac4919934a7b61b3ca68a4108c393e Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 00:18:41 +0300
Subject: [PATCH 39/88] fix(cli): object-to-string conversion errors
---
.../Framework/Writers/CommandClassWriter.php | 8 +++----
.../Writers/MiddlewareClassWriter.php | 20 ++++++++--------
.../Writers/SchedulerTaskClassWriter.php | 20 ++++++++--------
.../Framework/Writers/ThemeClassWriter.php | 24 +++++++++----------
4 files changed, 36 insertions(+), 36 deletions(-)
diff --git a/WebFiori/Framework/Writers/CommandClassWriter.php b/WebFiori/Framework/Writers/CommandClassWriter.php
index 41576558e..75d2a3b3e 100644
--- a/WebFiori/Framework/Writers/CommandClassWriter.php
+++ b/WebFiori/Framework/Writers/CommandClassWriter.php
@@ -132,9 +132,9 @@ public function writeClassBody() {
$this->append([
'/**',
' * Execute the command.',
- ' */',
- $this->f('exec', [], 'int'),
+ ' */'
], 1);
+ $this->f('exec', [], 'int');
$this->append([
'//TODO: Write the logic of the command.',
'return 0;',
@@ -176,9 +176,9 @@ protected function writeConstructor(array $params = [],
$this->append([
'/**',
' * Creates new instance of the class.',
- ' */',
- $this->f('__construct')
+ ' */'
], 1);
+ $this->f('__construct');
if (count($this->args) > 0) {
$this->append(["parent::__construct('$this->name', ["], 2);
diff --git a/WebFiori/Framework/Writers/MiddlewareClassWriter.php b/WebFiori/Framework/Writers/MiddlewareClassWriter.php
index 68eaa80f6..d615f1331 100644
--- a/WebFiori/Framework/Writers/MiddlewareClassWriter.php
+++ b/WebFiori/Framework/Writers/MiddlewareClassWriter.php
@@ -133,26 +133,26 @@ public function writeClassBody() {
$this->append([
'/**',
' * Execute a set of instructions before accessing the application.',
- ' */',
- $this->f('before', ['request' => 'Request', 'response' => 'Response']),
-
+ ' */'
], 1);
+ $this->f('before', ['request' => 'Request', 'response' => 'Response']);
+
$this->append('//TODO: Implement the action to perform before processing the request.', 2);
$this->append([
'}',
'/**',
' * Execute a set of instructions after processing the request and before sending back the response.',
- ' */',
- $this->f('after', ['request' => 'Request', 'response' => 'Response']),
+ ' */'
], 1);
+ $this->f('after', ['request' => 'Request', 'response' => 'Response']);
$this->append('//TODO: Implement the action to perform after processing the request.', 2);
$this->append([
'}',
'/**',
' * Execute a set of instructions after sending the response.',
- ' */',
- $this->f('afterSend', ['request' => 'Request', 'response' => 'Response']),
+ ' */'
], 1);
+ $this->f('afterSend', ['request' => 'Request', 'response' => 'Response']);
$this->append('//TODO: Implement the action to perform after sending the request.', 2);
$this->append('}', 1);
@@ -191,10 +191,10 @@ protected function writeConstructor(array $params = [],
$this->append([
'/**',
' * Creates new instance of the class.',
- ' */',
- $this->f('__construct'),
-
+ ' */'
], 1);
+ $this->f('__construct');
+
$this->append("parent::__construct('$this->name');", 2);
$this->append("\$this->setPriority($this->priority);", 2);
diff --git a/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php b/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
index 83e3c359f..4058467bd 100644
--- a/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
+++ b/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php
@@ -126,35 +126,35 @@ public function writeClassBody() {
$this->append([
'/**',
' * Execute the process.',
- ' */',
- $this->f('execute')
+ ' */'
], 1);
+ $this->f('execute');
$this->append('//TODO: Write the code that represents the process.', 2);
$this->append([
'}',
'/**',
' * Execute a set of instructions when the task failed to complete without errors.',
- ' */',
- $this->f('onFail')
+ ' */'
], 1);
+ $this->f('onFail');
$this->append('//TODO: Implement the action to perform when the task fails to complete without errors.', 2);
$this->append([
'}',
'/**',
' * Execute a set of instructions when the task completed without errors.',
- ' */',
- $this->f('onSuccess'),
+ ' */'
], 1);
+ $this->f('onSuccess');
$this->append('//TODO: Implement the action to perform when the task executes without errors.', 2);
$this->append([
'}',
'/**',
' * Execute a set of instructions after the task has finished to execute.',
- ' */',
- $this->f('afterExec'),
+ ' */'
], 1);
+ $this->f('afterExec');
$this->append('//TODO: Implement the action to perform when the task finishes to execute.', 2);
$this->append("//\$email = new TaskStatusEmail('no-reply', [", 2);
@@ -202,9 +202,9 @@ protected function writeConstructor(array $params = [],
$this->append([
'/**',
' * Creates new instance of the class.',
- ' */',
- $this->f('__construct')
+ ' */'
], 1);
+ $this->f('__construct');
$this->append([
"parent::__construct('".$this->getTaskName()."');",
"\$this->setDescription('".str_replace('\'', '\\\'', $this->getTaskDescription())."');"
diff --git a/WebFiori/Framework/Writers/ThemeClassWriter.php b/WebFiori/Framework/Writers/ThemeClassWriter.php
index 959c33bf4..cf2c365ad 100644
--- a/WebFiori/Framework/Writers/ThemeClassWriter.php
+++ b/WebFiori/Framework/Writers/ThemeClassWriter.php
@@ -89,9 +89,9 @@ public function writeClassBody() {
$this->append([
"/**",
" * Creates new instance of the class.",
- " */",
- $this->f('__construct')
+ " */"
], 1);
+ $this->f('__construct');
$this->append([
"parent::__construct('".$this->name."');",
'//TODO: Set the properties of your theme.',
@@ -113,9 +113,9 @@ public function writeClassBody() {
' *',
" * @return HTMLNode|null An object of type 'HTMLNode'. If the theme has no aside",
' * section, the method might return null.',
- ' */',
- $this->f('getAsideNode', [], 'HTMLNode'),
+ ' */'
], 1);
+ $this->f('getAsideNode', [], 'HTMLNode');
$this->append('return new AsideSection();', 2);
$this->append('}', 1);
$this->writeComponent('AsideSection', 'HTMLNode', 'A class that represents aside area of the theme.', 'Implement aside section of the theme.');
@@ -126,9 +126,9 @@ public function writeClassBody() {
' *',
" * @return HTMLNode|null An object of type 'HTMLNode'. If the theme has no footer",
' * section, the method might return null.',
- ' */',
- $this->f('getFooterNode', [], 'HTMLNode'),
+ ' */'
], 1);
+ $this->f('getFooterNode', [], 'HTMLNode');
$this->append('return new FooterSection();', 2);
$this->append('}', 1);
$this->writeComponent('FooterSection', 'HTMLNode', 'A class that represents footer section of the theme.', 'Implement footer section of the theme.');
@@ -144,15 +144,15 @@ public function writeClassBody() {
if (PHP_VERSION_ID <= 70333) {
$this->append([
" * @return HeadNode",
- ' */',
- $this->f('getHeadNode', [], 'HeadNode'),
+ ' */'
], 1);
+ $this->f('getHeadNode', [], 'HeadNode');
} else {
$this->append([
" * @return HeadSection",
- ' */',
- $this->f('getHeadNode', [], 'HeadSection'),
+ ' */'
], 1);
+ $this->f('getHeadNode', [], 'HeadSection');
}
$this->append('return new HeadSection();', 2);
$this->append('}', 1);
@@ -164,9 +164,9 @@ public function writeClassBody() {
' *',
" * @return HTMLNode|null @return HTMLNode|null An object of type 'HTMLNode'. If the theme has no header",
' * section, the method might return null.',
- ' */',
- $this->f('getHeaderNode', [], 'HTMLNode'),
+ ' */'
], 1);
+ $this->f('getHeaderNode', [], 'HTMLNode');
$this->append('return new HeaderSection();', 2);
$this->append('}', 1);
$this->writeComponent('HeaderSection', 'HTMLNode', 'A class that represents the top section of the theme.', 'Add header components such as navigation links.');
From 0e6efc7437e6a1e7674e65f6cfe2fe82c6d693ac Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 00:32:54 +0300
Subject: [PATCH 40/88] test: Fix test Expectations
---
.../Framework/Writers/LangClassWriter.php | 4 +-
.../Framework/Tests/Cli/HelpCommandTest.php | 1 -
.../Writers/CLICommandClassWriterTest.php | 44 ++++++++++++++++---
3 files changed, 40 insertions(+), 9 deletions(-)
diff --git a/WebFiori/Framework/Writers/LangClassWriter.php b/WebFiori/Framework/Writers/LangClassWriter.php
index 9b9514ffa..eaa477138 100644
--- a/WebFiori/Framework/Writers/LangClassWriter.php
+++ b/WebFiori/Framework/Writers/LangClassWriter.php
@@ -44,9 +44,9 @@ public function writeClassBody() {
$this->append([
"/**",
" * Creates new instance of the class.",
- " */",
- $this->f('__construct'),
+ " */"
], 1);
+ $this->f('__construct');
$this->append([
'parent::__construct(\''.$this->dir.'\', \''.$this->code.'\', true);',
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 2f3941bbe..46d02b76a 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -21,7 +21,6 @@ public function test00() {
" v: Display framework version info.\n",
" scheduler: Run tasks scheduler.\n",
- " create: Creates a system entity (middleware, web service, background process ...).\n",
" add: Add a database connection or SMTP account.\n",
diff --git a/tests/WebFiori/Framework/Tests/Writers/CLICommandClassWriterTest.php b/tests/WebFiori/Framework/Tests/Writers/CLICommandClassWriterTest.php
index 9c02dc46a..4c41f73f3 100644
--- a/tests/WebFiori/Framework/Tests/Writers/CLICommandClassWriterTest.php
+++ b/tests/WebFiori/Framework/Tests/Writers/CLICommandClassWriterTest.php
@@ -33,9 +33,25 @@ public function test01() {
$this->assertFalse($writer->setCommandName(' '));
$this->assertTrue($writer->setCommandName('Lets-Do-It'));
$this->assertEquals('Lets-Do-It', $writer->getCommandName());
- $this->assertFalse($writer->setClassName('Invalid Name'));
- $this->assertFalse($writer->setClassName(' '));
- $this->assertTrue($writer->setClassName('DoItXCommand'));
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("Invalid class name 'Invalid Name'. Class names must start with a letter or underscore, followed by letters, numbers, or underscores.");
+ $writer->setClassName('Invalid Name');
+ }
+
+ public function test01a() {
+ $writer = new CommandClassWriter();
+ $this->assertTrue($writer->setCommandName('Lets-Do-It'));
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("Invalid class name ' '. Class names must start with a letter or underscore, followed by letters, numbers, or underscores.");
+ $writer->setClassName(' ');
+ }
+
+ public function test01b() {
+ $writer = new CommandClassWriter();
+ $this->assertTrue($writer->setCommandName('Lets-Do-It'));
+ $writer->setClassName('DoItXCommand');
$this->assertEquals('DoItXCommand', $writer->getName());
$this->assertEquals(ROOT_PATH.DS.APP_DIR.DS.'Commands'.DS.'DoItXCommand.php', $writer->getAbsolutePath());
$this->assertEquals('App\\Commands', $writer->getNamespace());
@@ -76,9 +92,25 @@ public function test02() {
$this->assertFalse($writer->setCommandName(' '));
$this->assertTrue($writer->setCommandName('Lets-Do-It'));
$this->assertEquals('Lets-Do-It', $writer->getCommandName());
- $this->assertFalse($writer->setClassName('Invalid Name'));
- $this->assertFalse($writer->setClassName(' '));
- $this->assertTrue($writer->setClassName('DoItX2Command'));
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("Invalid class name 'Invalid Name'. Class names must start with a letter or underscore, followed by letters, numbers, or underscores.");
+ $writer->setClassName('Invalid Name');
+ }
+
+ public function test02a() {
+ $writer = new CommandClassWriter();
+ $this->assertTrue($writer->setCommandName('Lets-Do-It'));
+
+ $this->expectException(\InvalidArgumentException::class);
+ $this->expectExceptionMessage("Invalid class name ' '. Class names must start with a letter or underscore, followed by letters, numbers, or underscores.");
+ $writer->setClassName(' ');
+ }
+
+ public function test02b() {
+ $writer = new CommandClassWriter();
+ $this->assertTrue($writer->setCommandName('Lets-Do-It'));
+ $writer->setClassName('DoItX2Command');
$this->assertEquals('DoItX2Command', $writer->getName());
$this->assertEquals(ROOT_PATH.DS.APP_DIR.DS.'Commands'.DS.'DoItX2Command.php', $writer->getAbsolutePath());
$this->assertEquals('App\\Commands', $writer->getNamespace());
From 0d8058d142d04919d086cba65396bc7f8be6c6ac Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 16:27:37 +0300
Subject: [PATCH 41/88] refactor(cli): Split `add` Command
---
WebFiori/Framework/App.php | 4 +-
.../Framework/Cli/Commands/AddCommand.php | 196 ------------
.../Cli/Commands/AddDbConnectionCommand.php | 95 ++++++
.../Framework/Cli/Commands/AddLangCommand.php | 60 ++++
.../Cli/Commands/AddSmtpConnectionCommand.php | 82 +++++
.../Framework/Tests/Cli/AddCommandTest.php | 289 ------------------
.../Tests/Cli/AddDbConnectionCommandTest.php | 124 ++++++++
.../Tests/Cli/AddLangCommandTest.php | 82 +++++
.../Cli/AddSmtpConnectionCommandTest.php | 49 +++
.../Framework/Tests/Cli/HelpCommandTest.php | 12 +-
10 files changed, 502 insertions(+), 491 deletions(-)
delete mode 100644 WebFiori/Framework/Cli/Commands/AddCommand.php
create mode 100644 WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php
create mode 100644 WebFiori/Framework/Cli/Commands/AddLangCommand.php
create mode 100644 WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/AddCommandTest.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 0b8954e65..dc616d84c 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -445,7 +445,9 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\VersionCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\SchedulerCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\AddCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\AddDbConnectionCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\AddSmtpConnectionCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\AddLangCommand',
diff --git a/WebFiori/Framework/Cli/Commands/AddCommand.php b/WebFiori/Framework/Cli/Commands/AddCommand.php
deleted file mode 100644
index 60c08c4b6..000000000
--- a/WebFiori/Framework/Cli/Commands/AddCommand.php
+++ /dev/null
@@ -1,196 +0,0 @@
-select('What would you like to add?', $options, count($options) - 1);
-
- if ($answer == 'New database connection.') {
- return $this->addDbConnection();
- } else if ($answer == 'New SMTP connection.') {
- return $this->addSmtp();
- } else if ($answer == 'New website language.') {
- return $this->addLang();
- }
-
- return 0;
- }
- private function addDbConnection(): int {
- $dbType = $this->select('Select database type:', ConnectionInfo::SUPPORTED_DATABASES);
-
- $connInfoObj = new ConnectionInfo('mysql', 'root', 'pass', 'ok');
-
- if ($dbType == 'mssql') {
- $connInfoObj = new ConnectionInfo('mssql', 'root', 'pass', 'ok');
- }
-
- $connInfoObj->setHost($this->getInput('Database host:', '127.0.0.1'));
- $connInfoObj->setPort($this->getInput('Port number:', 3306));
- $connInfoObj->setUsername($this->getInput('Username:'));
- $connInfoObj->setPassword($this->getInput('Password:'));
- $connInfoObj->setDBName($this->getInput('Database name:'));
- $connInfoObj->setName($this->getInput('Give your connection a friendly name:', 'db-connection-'.(count(App::getConfig()->getDBConnections()) + 1)));
- $this->println('Trying to connect to the database...');
-
- $addConnection = $this->tryConnect($connInfoObj);
- $orgHost = $connInfoObj->getHost();
- $orgErr = $addConnection !== true ? $addConnection->getMessage() : '';
-
- if ($addConnection !== true) {
- if ($connInfoObj->getHost() == '127.0.0.1') {
- $this->println("Trying with 'localhost'...");
- $connInfoObj->setHost('localhost');
- $addConnection = $this->tryConnect($connInfoObj);
- } else if ($connInfoObj->getHost() == 'localhost') {
- $this->println("Trying with '127.0.0.1'...");
- $connInfoObj->setHost('127.0.0.1');
- $addConnection = $this->tryConnect($connInfoObj);
- }
- }
-
- if ($addConnection === true) {
- $this->success('Connected. Adding the connection...');
-
- App::getConfig()->addOrUpdateDBConnection($connInfoObj);
- $this->success('Connection information was stored in application configuration.');
- } else {
- $connInfoObj->setHost($orgHost);
- $this->error('Unable to connect to the database.');
- $this->error($orgErr);
- $this->confirmAdd($connInfoObj);
- }
-
- return 0;
- }
- private function addLang(): int {
- $langCode = strtoupper(trim($this->getInput('Language code:')));
-
- if (strlen($langCode) != 2) {
- $this->error('Invalid language code.');
-
- return -1;
- }
-
- if (App::getConfig()->getAppName($langCode) !== null) {
- $this->info('This language already added. Nothing changed.');
-
- return 0;
- }
- App::getConfig()->setAppName($this->getInput('Name of the website in the new language:'), $langCode);
- App::getConfig()->setDescription($this->getInput('Description of the website in the new language:'), $langCode);
- App::getConfig()->setTitle($this->getInput('Default page title in the new language:'), $langCode);
- $writingDir = $this->select('Select writing direction:', [
- 'ltr', 'rtl'
- ]);
-
- $writer = new LangClassWriter($langCode, $writingDir);
- $writer->writeClass();
- $this->success('Language added. Also, a class for the language '
- .'is created at "'.APP_DIR.'\Langs" for that language.');
-
- return 0;
- }
- private function addSmtp(): int {
- $smtpConn = new SMTPAccount();
- $smtpConn->setServerAddress($this->getInput('SMTP Server address:', '127.0.0.1'));
- $smtpConn->setPort(25);
- $addr = $smtpConn->getAddress();
-
- if ($addr == 'smtp.outlook.com'
- || $addr == 'outlook.office365.com'
- || $addr == 'smtp.office365.com') {
- $smtpConn->setPort(587);
- } else if ($addr == 'smtp.gmail.com'
- || $addr == 'smtp.mail.yahoo.com') {
- $smtpConn->setPort(465);
- }
- $smtpConn->setPort($this->getInput('Port number:', $smtpConn->getPort()));
- $smtpConn->setUsername($this->getInput('Username:'));
- $smtpConn->setPassword($this->getInput('Password:'));
- $smtpConn->setAddress($this->getInput('Sender email address:', $smtpConn->getUsername()));
- $smtpConn->setSenderName($this->getInput('Sender name:', $smtpConn->getAddress()));
- $smtpConn->setAccountName($this->getInput('Give your connection a friendly name:', 'smtp-connection-'.count(App::getConfig()->getSMTPConnections())));
- $this->println('Trying to connect. This can take up to 1 minute...');
- $server = new SMTPServer($smtpConn->getServerAddress(), $smtpConn->getPort());
-
- try {
- if ($server->authLogin($smtpConn->getUsername(), $smtpConn->getPassword())) {
- $this->success('Connected. Adding connection information...');
- App::getConfig()->addOrUpdateSMTPAccount($smtpConn);
- $this->success('Connection information was stored in application configuration.');
- } else {
- $this->error('Unable to connect to SMTP server.');
- $this->println('Error Information: '.$server->getLastResponse());
-
- $this->confirmAdd($smtpConn);
- }
- } catch (SMTPException $ex) {
- $this->error('An exception with message "'.$ex->getMessage().'" was thrown while trying to connect.');
- $this->confirmAdd($smtpConn);
- }
-
- return 0;
- }
- private function confirmAdd($smtpOrDbConn) {
- if ($this->confirm('Would you like to store connection information anyway?', false)) {
- if ($smtpOrDbConn instanceof SMTPAccount) {
- App::getConfig()->addOrUpdateSMTPAccount($smtpOrDbConn);
- } else if ($smtpOrDbConn instanceof ConnectionInfo) {
- App::getConfig()->addOrUpdateDBConnection($smtpOrDbConn);
- }
- $this->success('Connection information was stored in application configuration.');
- }
- }
- private function tryConnect($connectionInfo) {
- try {
- $db = new DB($connectionInfo);
- $db->getConnection();
-
- return true;
- } catch (DatabaseException $ex) {
- return $ex;
- }
- }
-}
diff --git a/WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php b/WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php
new file mode 100644
index 000000000..99cf432f5
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php
@@ -0,0 +1,95 @@
+select('Select database type:', ConnectionInfo::SUPPORTED_DATABASES);
+
+ $connInfoObj = new ConnectionInfo('mysql', 'root', 'pass', 'ok');
+
+ if ($dbType == 'mssql') {
+ $connInfoObj = new ConnectionInfo('mssql', 'root', 'pass', 'ok');
+ }
+
+ $connInfoObj->setHost($this->getInput('Database host:', '127.0.0.1'));
+ $connInfoObj->setPort($this->getInput('Port number:', 3306));
+ $connInfoObj->setUsername($this->getInput('Username:'));
+ $connInfoObj->setPassword($this->getInput('Password:'));
+ $connInfoObj->setDBName($this->getInput('Database name:'));
+ $connInfoObj->setName($this->getInput('Give your connection a friendly name:', 'db-connection-'.(count(App::getConfig()->getDBConnections()) + 1)));
+ $this->println('Trying to connect to the database...');
+
+ $addConnection = $this->tryConnect($connInfoObj);
+ $orgHost = $connInfoObj->getHost();
+ $orgErr = $addConnection !== true ? $addConnection->getMessage() : '';
+
+ if ($addConnection !== true) {
+ if ($connInfoObj->getHost() == '127.0.0.1') {
+ $this->println("Trying with 'localhost'...");
+ $connInfoObj->setHost('localhost');
+ $addConnection = $this->tryConnect($connInfoObj);
+ } else if ($connInfoObj->getHost() == 'localhost') {
+ $this->println("Trying with '127.0.0.1'...");
+ $connInfoObj->setHost('127.0.0.1');
+ $addConnection = $this->tryConnect($connInfoObj);
+ }
+ }
+
+ if ($addConnection === true) {
+ $this->success('Connected. Adding the connection...');
+
+ App::getConfig()->addOrUpdateDBConnection($connInfoObj);
+ $this->success('Connection information was stored in application configuration.');
+ } else {
+ $connInfoObj->setHost($orgHost);
+ $this->error('Unable to connect to the database.');
+ $this->error($orgErr);
+
+ if ($this->confirm('Would you like to store connection information anyway?', false)) {
+ App::getConfig()->addOrUpdateDBConnection($connInfoObj);
+ $this->success('Connection information was stored in application configuration.');
+ }
+ }
+
+ return 0;
+ }
+ private function tryConnect($connectionInfo) {
+ try {
+ $db = new DB($connectionInfo);
+ $db->getConnection();
+
+ return true;
+ } catch (DatabaseException $ex) {
+ return $ex;
+ }
+ }
+}
diff --git a/WebFiori/Framework/Cli/Commands/AddLangCommand.php b/WebFiori/Framework/Cli/Commands/AddLangCommand.php
new file mode 100644
index 000000000..92e31f5c5
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/AddLangCommand.php
@@ -0,0 +1,60 @@
+getInput('Language code:')));
+
+ if (strlen($langCode) != 2) {
+ $this->error('Invalid language code.');
+
+ return -1;
+ }
+
+ if (App::getConfig()->getAppName($langCode) !== null) {
+ $this->info('This language already added. Nothing changed.');
+
+ return 0;
+ }
+ App::getConfig()->setAppName($this->getInput('Name of the website in the new language:'), $langCode);
+ App::getConfig()->setDescription($this->getInput('Description of the website in the new language:'), $langCode);
+ App::getConfig()->setTitle($this->getInput('Default page title in the new language:'), $langCode);
+ $writingDir = $this->select('Select writing direction:', [
+ 'ltr', 'rtl'
+ ]);
+
+ $writer = new LangClassWriter($langCode, $writingDir);
+ $writer->writeClass();
+ $this->success('Language added. Also, a class for the language '
+ .'is created at "'.APP_DIR.'\Langs" for that language.');
+
+ return 0;
+ }
+}
diff --git a/WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php b/WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php
new file mode 100644
index 000000000..3fbb68ee5
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php
@@ -0,0 +1,82 @@
+setServerAddress($this->getInput('SMTP Server address:', '127.0.0.1'));
+ $smtpConn->setPort(25);
+ $addr = $smtpConn->getAddress();
+
+ if ($addr == 'smtp.outlook.com'
+ || $addr == 'outlook.office365.com'
+ || $addr == 'smtp.office365.com') {
+ $smtpConn->setPort(587);
+ } else if ($addr == 'smtp.gmail.com'
+ || $addr == 'smtp.mail.yahoo.com') {
+ $smtpConn->setPort(465);
+ }
+ $smtpConn->setPort($this->getInput('Port number:', $smtpConn->getPort()));
+ $smtpConn->setUsername($this->getInput('Username:'));
+ $smtpConn->setPassword($this->getInput('Password:'));
+ $smtpConn->setAddress($this->getInput('Sender email address:', $smtpConn->getUsername()));
+ $smtpConn->setSenderName($this->getInput('Sender name:', $smtpConn->getAddress()));
+ $smtpConn->setAccountName($this->getInput('Give your connection a friendly name:', 'smtp-connection-'.count(App::getConfig()->getSMTPConnections())));
+ $this->println('Trying to connect. This can take up to 1 minute...');
+ $server = new SMTPServer($smtpConn->getServerAddress(), $smtpConn->getPort());
+
+ try {
+ if ($server->authLogin($smtpConn->getUsername(), $smtpConn->getPassword())) {
+ $this->success('Connected. Adding connection information...');
+ App::getConfig()->addOrUpdateSMTPAccount($smtpConn);
+ $this->success('Connection information was stored in application configuration.');
+ } else {
+ $this->error('Unable to connect to SMTP server.');
+ $this->println('Error Information: '.$server->getLastResponse());
+
+ if ($this->confirm('Would you like to store connection information anyway?', false)) {
+ App::getConfig()->addOrUpdateSMTPAccount($smtpConn);
+ $this->success('Connection information was stored in application configuration.');
+ }
+ }
+ } catch (SMTPException $ex) {
+ $this->error('An exception with message "'.$ex->getMessage().'" was thrown while trying to connect.');
+
+ if ($this->confirm('Would you like to store connection information anyway?', false)) {
+ App::getConfig()->addOrUpdateSMTPAccount($smtpConn);
+ $this->success('Connection information was stored in application configuration.');
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddCommandTest.php
deleted file mode 100644
index 35559ab3d..000000000
--- a/tests/WebFiori/Framework/Tests/Cli/AddCommandTest.php
+++ /dev/null
@@ -1,289 +0,0 @@
-executeSingleCommand(new AddCommand(), [], [
- '3'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to add?\n",
- "0: New database connection.\n",
- "1: New SMTP connection.\n",
- "2: New website language.\n",
- "3: Quit. <--\n"
- ], $output);
- }
- /**
- * @test
- */
- public function testAddDBConnection00() {
- $output = $this->executeSingleCommand(new AddCommand(), [], [
- '0',
- '0',
- '127.0.0.1',
- "\n", // Hit Enter to pick default value (port 3306)
- 'root',
- '123456',
- 'testing_db',
- "\n" // Hit Enter to pick default value (connection name)
- ]);
-
- $count = count(App::getConfig()->getDBConnections());
- $connName = 'db-connection-'.$count;
- $this->assertEquals([
- "What would you like to add?\n",
- "0: New database connection.\n",
- "1: New SMTP connection.\n",
- "2: New website language.\n",
- "3: Quit. <--\n",
- "Select database type:\n",
- "0: mysql\n",
- "1: mssql\n",
- "Database host: Enter = '127.0.0.1'\n",
- "Port number: Enter = '3306'\n",
- "Username:\n",
- "Password:\n",
- "Database name:\n",
- "Give your connection a friendly name: Enter = '$connName'\n",
- "Trying to connect to the database...\n",
- "Success: Connected. Adding the connection...\n",
- "Success: Connection information was stored in application configuration.\n"
- ], $output);
- $this->assertEquals(0, $this->getExitCode());
- }
- /**
- * @test
- */
- public function testAddDBConnection01() {
- $connName = 'db-connection-'.(count(App::getConfig()->getDBConnections()) + 1);
-
- $output = $this->executeSingleCommand(new AddCommand(), [
- 'WebFiori',
- 'add'
- ], [
- '0',
- '0',
- '127.0.0.1',
- "\n", // Hit Enter to pick default value (port 3306)
- 'root',
- 'not_correct',
- 'testing_db',
- "\n", // Hit Enter to pick default value (connection name)
- 'y'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $output = $this->getOutput();
-
- $this->assertEquals("What would you like to add?\n", $output[0]);
- $this->assertEquals("0: New database connection.\n", $output[1]);
- $this->assertEquals("1: New SMTP connection.\n", $output[2]);
- $this->assertEquals("2: New website language.\n", $output[3]);
- $this->assertEquals("3: Quit. <--\n", $output[4]);
- $this->assertEquals("Select database type:\n", $output[5]);
- $this->assertEquals("0: mysql\n", $output[6]);
- $this->assertEquals("1: mssql\n", $output[7]);
- $this->assertEquals("Database host: Enter = '127.0.0.1'\n", $output[8]);
- $this->assertEquals("Port number: Enter = '3306'\n", $output[9]);
- $this->assertEquals("Username:\n", $output[10]);
- $this->assertEquals("Password:\n", $output[11]);
- $this->assertEquals("Database name:\n", $output[12]);
- $this->assertEquals("Give your connection a friendly name: Enter = '$connName'\n", $output[13]);
- $this->assertEquals("Trying to connect to the database...\n", $output[14]);
- $this->assertEquals("Trying with 'localhost'...\n", $output[15]);
- $this->assertEquals("Error: Unable to connect to the database.\n", $output[16]);
- $this->assertStringContainsString("Error: Unable to connect to database: 1045 - Access denied for user", $output[17]);
- $this->assertEquals("Would you like to store connection information anyway?(y/N)\n", $output[18]);
- $this->assertEquals("Success: Connection information was stored in application configuration.\n", $output[19]);
- }
- /**
- * @test
- */
- public function testAddDBConnection02() {
- $count = count(App::getConfig()->getDBConnections());
- $connName = 'db-connection-'.($count + 1);
-
- $output = $this->executeSingleCommand(new AddCommand(), [
- 'WebFiori',
- 'add'
- ], [
- '0',
- '0',
- '127.0.0.1',
- "\n", // Hit Enter to pick default value (port 3306)
- 'root',
- 'not_correct',
- 'testing_db',
- "\n", // Hit Enter to pick default value (connection name)
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $output = $this->getOutput();
-
- $this->assertEquals("What would you like to add?\n", $output[0]);
- $this->assertEquals("0: New database connection.\n", $output[1]);
- $this->assertEquals("1: New SMTP connection.\n", $output[2]);
- $this->assertEquals("2: New website language.\n", $output[3]);
- $this->assertEquals("3: Quit. <--\n", $output[4]);
- $this->assertEquals("Select database type:\n", $output[5]);
- $this->assertEquals("0: mysql\n", $output[6]);
- $this->assertEquals("1: mssql\n", $output[7]);
- $this->assertEquals("Database host: Enter = '127.0.0.1'\n", $output[8]);
- $this->assertEquals("Port number: Enter = '3306'\n", $output[9]);
- $this->assertEquals("Username:\n", $output[10]);
- $this->assertEquals("Password:\n", $output[11]);
- $this->assertEquals("Database name:\n", $output[12]);
- $this->assertEquals("Give your connection a friendly name: Enter = '$connName'\n", $output[13]);
- $this->assertEquals("Trying to connect to the database...\n", $output[14]);
- $this->assertEquals("Trying with 'localhost'...\n", $output[15]);
- $this->assertEquals("Error: Unable to connect to the database.\n", $output[16]);
- $this->assertStringContainsString("Error: Unable to connect to database: 1045 - Access denied for user", $output[17]);
- $this->assertEquals("Would you like to store connection information anyway?(y/N)\n", $output[18]);
- }
-
- /**
- * @test
- */
- public function testAddLang00() {
- // Generate a unique 2-character language code based on current microseconds
- $langCode = substr(str_replace('.', '', microtime(true)), -2);
- // Ensure it's exactly 2 characters and alphabetic
- $langCode = chr(65 + ($langCode[0] % 26)) . chr(65 + ($langCode[1] % 26));
-
- // Clean up if it exists from previous runs
- if (class_exists('\\App\\Langs\\Lang' . $langCode)) {
- $this->removeClass('\\App\\Langs\\Lang' . $langCode);
- }
-
- $output = $this->executeSingleCommand(new AddCommand(), [], [
- '2',
- $langCode,
- 'F Name',
- 'F description',
- 'Default f Title',
- 'ltr',
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to add?\n",
- "0: New database connection.\n",
- "1: New SMTP connection.\n",
- "2: New website language.\n",
- "3: Quit. <--\n",
- "Language code:\n",
- "Name of the website in the new language:\n",
- "Description of the website in the new language:\n",
- "Default page title in the new language:\n",
- "Select writing direction:\n",
- "0: ltr\n",
- "1: rtl\n",
- "Success: Language added. Also, a class for the language is created at \"".APP_DIR."\Langs\" for that language.\n"
- ], $output);
- $this->assertTrue(class_exists('\\App\\Langs\\Lang' . $langCode));
- $this->removeClass('\\App\\Langs\\Lang' . $langCode);
- Controller::getDriver()->initialize();
- }
- /**
- * @test
- */
- public function testAddLang01() {
- $output = $this->executeSingleCommand(new AddCommand(), [], [
- '2',
- 'EN',
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to add?\n",
- "0: New database connection.\n",
- "1: New SMTP connection.\n",
- "2: New website language.\n",
- "3: Quit. <--\n",
- "Language code:\n",
- "Info: This language already added. Nothing changed.\n",
- ], $output);
- Controller::getDriver()->initialize();
- }
- /**
- * @test
- */
- public function testAddLang02() {
- $output = $this->executeSingleCommand(new AddCommand(), [], [
- '2',
- 'FKRR',
- ]);
-
- $this->assertEquals(-1, $this->getExitCode());
- $this->assertEquals([
- "What would you like to add?\n",
- "0: New database connection.\n",
- "1: New SMTP connection.\n",
- "2: New website language.\n",
- "3: Quit. <--\n",
- "Language code:\n",
- "Error: Invalid language code.\n",
- ], $output);
- $this->removeClass('\\App\\Langs\\LanguageFK');
- }
- /**
- * @test
- */
- public function testAddSMTPConnection00() {
- $connName = 'smtp-connection-'.count(App::getConfig()->getSMTPConnections());
-
- $output = $this->executeSingleCommand(new AddCommand(), [
- 'WebFiori',
- 'add'
- ], [
- '1',
- '127.0.0.1',
- "\n", // Hit Enter to pick default value (port 25)
- 'test@example.com',
- getenv('MYSQL_ROOT_PASSWORD') ?: '12345326',
- 'test@example.com',
- 'test@example.com',
- "\n", // Hit Enter to pick default value (connection name)
- 'n'
- ]);
-
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "What would you like to add?\n",
- "0: New database connection.\n",
- "1: New SMTP connection.\n",
- "2: New website language.\n",
- "3: Quit. <--\n",
- "SMTP Server address: Enter = '127.0.0.1'\n",
- "Port number: Enter = '25'\n",
- "Username:\n",
- "Password:\n",
- "Sender email address: Enter = 'test@example.com'\n",
- "Sender name: Enter = 'test@example.com'\n",
- "Give your connection a friendly name: Enter = '$connName'\n",
- "Trying to connect. This can take up to 1 minute...\n",
- "Error: Unable to connect to SMTP server.\n",
- "Error Information: \n",
- "Would you like to store connection information anyway?(y/N)\n",
- ], $output);
- }
-
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
new file mode 100644
index 000000000..6734208ab
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
@@ -0,0 +1,124 @@
+executeSingleCommand(new AddDbConnectionCommand(), [], [
+ '0',
+ '127.0.0.1',
+ "\n", // Hit Enter to pick default value (port 3306)
+ 'root',
+ '123456',
+ 'testing_db',
+ "\n" // Hit Enter to pick default value (connection name)
+ ]);
+
+ $count = count(App::getConfig()->getDBConnections());
+ $connName = 'db-connection-'.$count;
+ $this->assertEquals([
+ "Select database type:\n",
+ "0: mysql\n",
+ "1: mssql\n",
+ "Database host: Enter = '127.0.0.1'\n",
+ "Port number: Enter = '3306'\n",
+ "Username:\n",
+ "Password:\n",
+ "Database name:\n",
+ "Give your connection a friendly name: Enter = '$connName'\n",
+ "Trying to connect to the database...\n",
+ "Success: Connected. Adding the connection...\n",
+ "Success: Connection information was stored in application configuration.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+ /**
+ * @test
+ */
+ public function testAddDBConnection01() {
+ $connName = 'db-connection-'.(count(App::getConfig()->getDBConnections()) + 1);
+
+ $output = $this->executeSingleCommand(new AddDbConnectionCommand(), [
+ 'WebFiori',
+ 'add:db-connection'
+ ], [
+ '0',
+ '127.0.0.1',
+ "\n", // Hit Enter to pick default value (port 3306)
+ 'root',
+ 'not_correct',
+ 'testing_db',
+ "\n", // Hit Enter to pick default value (connection name)
+ 'y'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $output = $this->getOutput();
+
+ $this->assertEquals("Select database type:\n", $output[0]);
+ $this->assertEquals("0: mysql\n", $output[1]);
+ $this->assertEquals("1: mssql\n", $output[2]);
+ $this->assertEquals("Database host: Enter = '127.0.0.1'\n", $output[3]);
+ $this->assertEquals("Port number: Enter = '3306'\n", $output[4]);
+ $this->assertEquals("Username:\n", $output[5]);
+ $this->assertEquals("Password:\n", $output[6]);
+ $this->assertEquals("Database name:\n", $output[7]);
+ $this->assertEquals("Give your connection a friendly name: Enter = '$connName'\n", $output[8]);
+ $this->assertEquals("Trying to connect to the database...\n", $output[9]);
+ $this->assertEquals("Trying with 'localhost'...\n", $output[10]);
+ $this->assertEquals("Error: Unable to connect to the database.\n", $output[11]);
+ $this->assertStringContainsString("Error: Unable to connect to database: 1045 - Access denied for user", $output[12]);
+ $this->assertEquals("Would you like to store connection information anyway?(y/N)\n", $output[13]);
+ $this->assertEquals("Success: Connection information was stored in application configuration.\n", $output[14]);
+ }
+ /**
+ * @test
+ */
+ public function testAddDBConnection02() {
+ $count = count(App::getConfig()->getDBConnections());
+ $connName = 'db-connection-'.($count + 1);
+
+ $output = $this->executeSingleCommand(new AddDbConnectionCommand(), [
+ 'WebFiori',
+ 'add:db-connection'
+ ], [
+ '0',
+ '127.0.0.1',
+ "\n", // Hit Enter to pick default value (port 3306)
+ 'root',
+ 'not_correct',
+ 'testing_db',
+ "\n", // Hit Enter to pick default value (connection name)
+ 'n'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $output = $this->getOutput();
+
+ $this->assertEquals("Select database type:\n", $output[0]);
+ $this->assertEquals("0: mysql\n", $output[1]);
+ $this->assertEquals("1: mssql\n", $output[2]);
+ $this->assertEquals("Database host: Enter = '127.0.0.1'\n", $output[3]);
+ $this->assertEquals("Port number: Enter = '3306'\n", $output[4]);
+ $this->assertEquals("Username:\n", $output[5]);
+ $this->assertEquals("Password:\n", $output[6]);
+ $this->assertEquals("Database name:\n", $output[7]);
+ $this->assertEquals("Give your connection a friendly name: Enter = '$connName'\n", $output[8]);
+ $this->assertEquals("Trying to connect to the database...\n", $output[9]);
+ $this->assertEquals("Trying with 'localhost'...\n", $output[10]);
+ $this->assertEquals("Error: Unable to connect to the database.\n", $output[11]);
+ $this->assertStringContainsString("Error: Unable to connect to database: 1045 - Access denied for user", $output[12]);
+ $this->assertEquals("Would you like to store connection information anyway?(y/N)\n", $output[13]);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php
new file mode 100644
index 000000000..a78496880
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php
@@ -0,0 +1,82 @@
+removeClass('\\App\\Langs\\Lang' . $langCode);
+ }
+
+ $output = $this->executeSingleCommand(new AddLangCommand(), [], [
+ $langCode,
+ 'F Name',
+ 'F description',
+ 'Default f Title',
+ 'ltr',
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Language code:\n",
+ "Name of the website in the new language:\n",
+ "Description of the website in the new language:\n",
+ "Default page title in the new language:\n",
+ "Select writing direction:\n",
+ "0: ltr\n",
+ "1: rtl\n",
+ "Success: Language added. Also, a class for the language is created at \"".APP_DIR."\Langs\" for that language.\n"
+ ], $output);
+ $this->assertTrue(class_exists('\\App\\Langs\\Lang' . $langCode));
+ $this->removeClass('\\App\\Langs\\Lang' . $langCode);
+ Controller::getDriver()->initialize();
+ }
+ /**
+ * @test
+ */
+ public function testAddLang01() {
+ $output = $this->executeSingleCommand(new AddLangCommand(), [], [
+ 'EN',
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Language code:\n",
+ "Info: This language already added. Nothing changed.\n",
+ ], $output);
+ Controller::getDriver()->initialize();
+ }
+ /**
+ * @test
+ */
+ public function testAddLang02() {
+ $output = $this->executeSingleCommand(new AddLangCommand(), [], [
+ 'FKRR',
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertEquals([
+ "Language code:\n",
+ "Error: Invalid language code.\n",
+ ], $output);
+ $this->removeClass('\\App\\Langs\\LanguageFK');
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
new file mode 100644
index 000000000..232e7fc8d
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
@@ -0,0 +1,49 @@
+getSMTPConnections());
+
+ $output = $this->executeSingleCommand(new AddSmtpConnectionCommand(), [
+ 'WebFiori',
+ 'add:smtp-connection'
+ ], [
+ '127.0.0.1',
+ "\n", // Hit Enter to pick default value (port 25)
+ 'test@example.com',
+ getenv('MYSQL_ROOT_PASSWORD') ?: '12345326',
+ 'test@example.com',
+ 'test@example.com',
+ "\n", // Hit Enter to pick default value (connection name)
+ 'n'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "SMTP Server address: Enter = '127.0.0.1'\n",
+ "Port number: Enter = '25'\n",
+ "Username:\n",
+ "Password:\n",
+ "Sender email address: Enter = 'test@example.com'\n",
+ "Sender name: Enter = 'test@example.com'\n",
+ "Give your connection a friendly name: Enter = '$connName'\n",
+ "Trying to connect. This can take up to 1 minute...\n",
+ "Error: Unable to connect to SMTP server.\n",
+ "Error Information: \n",
+ "Would you like to store connection information anyway?(y/N)\n",
+ ], $output);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 46d02b76a..03936fb0e 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -17,17 +17,19 @@ public function test00() {
"Global Arguments:\n",
" --ansi:[Optional] Force the use of ANSI output.\n",
"Available Commands:\n",
- " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n",
- " v: Display framework version info.\n",
+ " help: Display CLI Help. To display help for specific command, use the argument \"--command\" with this command.\n",
+ " v: Display framework version info.\n",
- " scheduler: Run tasks scheduler.\n",
- " add: Add a database connection or SMTP account.\n",
+ " scheduler: Run tasks scheduler.\n",
+ " add:db-connection: Add a database connection.\n",
+ " add:smtp-connection: Add an SMTP account.\n",
+ " add:lang: Add a website language.\n",
- " migrations: Execute database migrations.\n",
+ " migrations: Execute database migrations.\n",
], $this->executeMultiCommand([
'help',
]));
From 2b189be7fc0404942db188cbd60d5a6be01f43e2 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 17:30:03 +0300
Subject: [PATCH 42/88] feat: Create Middleware Command
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateMiddlewareCommand.php | 121 +++++++++++
.../Tests/Cli/CreateMiddlewareCommandTest.php | 203 ++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
4 files changed, 326 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index dc616d84c..ed0bf9512 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -448,6 +448,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\AddDbConnectionCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\AddSmtpConnectionCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\AddLangCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateMiddlewareCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php b/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php
new file mode 100644
index 000000000..9d44eb6fd
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php
@@ -0,0 +1,121 @@
+getArgValue('--priority');
+
+ if ($priority === null) {
+ $validator = new InputValidator(function($input) {
+ return is_numeric($input);
+ }, 'Priority must be a number.');
+
+ $priority = (int)$this->getInput('Enter middleware priority:', 0, $validator);
+ } else {
+ if (!is_numeric($priority)) {
+ $this->error('Priority must be a number.');
+ return -1;
+ }
+ $priority = (int)$priority;
+ }
+ return $priority;
+ }
+ public function getGroups() : array {
+ $groupsArg = $this->getArgValue('--groups');
+ $groups = [];
+
+ if ($groupsArg !== null) {
+ if (!empty($groupsArg)) {
+ $groups = array_map('trim', explode(',', $groupsArg));
+ $groups = array_filter($groups, fn($g) => !empty($g));
+ }
+ } else {
+ if ($this->confirm('Add middleware to groups?', false)) {
+ while (true) {
+ $group = $this->getInput('Enter group name (leave empty to finish):');
+ if (empty(trim($group))) {
+ break;
+ }
+ $groups[] = trim($group);
+ }
+ }
+ }
+ return $groups;
+ }
+ private function getMDName(string $className) : string {
+ $middlewareName = $this->getArgValue('--name');
+
+ if ($middlewareName === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Middleware name cannot be empty.');
+
+ $middlewareName = $this->getInput('Enter middleware name:', $className, $validator);
+ }
+ return $middlewareName;
+ }
+ /**
+ * Execute the command.
+ *
+ * @return int
+ */
+ public function exec() : int {
+ $className = $this->getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = trim($this->getInput('Enter middleware class name:', null, $validator));
+ }
+
+
+
+ $middlewareName = $this->getMDName($className);
+ $priority = $this->getPriority();
+
+ if ($priority === -1) {
+ return -1;
+ }
+
+ $groups = $this->getGroups();
+
+
+ $writer = new MiddlewareClassWriter($middlewareName, $priority, $groups);
+ $writer->setClassName($className);
+ $writer->writeClass();
+
+ $this->success('Middleware class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
new file mode 100644
index 000000000..c12f7123d
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
@@ -0,0 +1,203 @@
+executeSingleCommand(new CreateMiddlewareCommand(), [], [
+ $className,
+ "\n", // Use default middleware name (same as class name)
+ "\n", // Use default priority (0)
+ 'n' // Don't add to groups
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter middleware class name:\n",
+ "Enter middleware name: Enter = '$className'\n",
+ "Enter middleware priority: Enter = '0'\n",
+ "Add middleware to groups?(y/N)\n",
+ "Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
+ $this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
+ }
+ /**
+ * @test
+ */
+ public function testCreateMiddleware01() {
+ $className = 'TestMd'.time();
+
+ $output = $this->executeSingleCommand(new CreateMiddlewareCommand(), [], [
+ $className,
+ 'My Custom Middleware',
+ '100',
+ 'y', // Add to groups
+ 'api',
+ 'web',
+ "\n" // Empty to finish adding groups
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $output = $this->getOutput();
+
+ $this->assertEquals("Enter middleware class name:\n", $output[0]);
+ $this->assertEquals("Enter middleware name: Enter = '$className'\n", $output[1]);
+ $this->assertEquals("Enter middleware priority: Enter = '0'\n", $output[2]);
+ $this->assertEquals("Add middleware to groups?(y/N)\n", $output[3]);
+ $this->assertEquals("Enter group name (leave empty to finish):\n", $output[4]);
+ $this->assertEquals("Enter group name (leave empty to finish):\n", $output[5]);
+ $this->assertEquals("Enter group name (leave empty to finish):\n", $output[6]);
+ $this->assertEquals("Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n", $output[7]);
+
+ $this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
+ $this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
+ }
+ /**
+ * @test
+ */
+ public function testCreateMiddleware02() {
+ $className = 'TestMd'.time();
+
+ $output = $this->executeSingleCommand(new CreateMiddlewareCommand(), [], [
+ '', // Empty class name - will be rejected
+ $className, // Valid class name
+ "\n", // Use default middleware name
+ "\n", // Use default priority
+ 'n' // No groups
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter middleware class name:\n",
+ "Error: Class name cannot be empty.\n",
+ "Enter middleware class name:\n",
+ "Enter middleware name: Enter = '$className'\n",
+ "Enter middleware priority: Enter = '0'\n",
+ "Add middleware to groups?(y/N)\n",
+ "Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
+ $this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
+ }
+ /**
+ * @test
+ */
+ public function testCreateMiddleware03() {
+ $className = 'TestMd'.time();
+
+ $output = $this->executeSingleCommand(new CreateMiddlewareCommand(), [
+ 'WebFiori',
+ 'create:middleware'
+ ], [
+ $className,
+ "\n", // Use default middleware name (same as class name)
+ '50',
+ 'n'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
+
+ $this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
+ }
+ /**
+ * @test
+ */
+ public function testCreateMiddlewareWithArgs00() {
+ $className = 'TestMd'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateMiddlewareCommand::class,
+ '--class-name' => $className,
+ '--name' => 'Auth Middleware',
+ '--priority' => '100',
+ '--groups' => ''
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n", $output);
+
+ $this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
+ $this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
+ }
+ /**
+ * @test
+ */
+ public function testCreateMiddlewareWithArgs01() {
+ $className = 'TestMd'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateMiddlewareCommand::class,
+ '--class-name' => $className,
+ '--name' => $className,
+ '--priority' => '0',
+ '--groups' => 'api,web,admin'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
+ $this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
+ }
+ /**
+ * @test
+ */
+ public function testCreateMiddlewareWithArgs02() {
+ $output = $this->executeMultiCommand([
+ CreateMiddlewareCommand::class,
+ '--class-name' => '',
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Class name cannot be empty.\n", $output);
+ }
+ /**
+ * @test
+ */
+ public function testCreateMiddlewareWithArgs03() {
+ $className = 'TestMd'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateMiddlewareCommand::class,
+ '--class-name' => $className,
+ '--name' => $className,
+ '--priority' => 'invalid',
+ '--groups' => ''
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Priority must be a number.\n", $output);
+ }
+ /**
+ * @test
+ */
+ public function testCreateMiddlewareWithArgs04() {
+ $className = 'TestMd'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateMiddlewareCommand::class,
+ '--class-name' => $className,
+ '--name' => 'My Middleware',
+ '--priority' => '50',
+ '--groups' => 'api,web'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
+ $this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 03936fb0e..1cd225d82 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -24,6 +24,7 @@ public function test00() {
" add:db-connection: Add a database connection.\n",
" add:smtp-connection: Add an SMTP account.\n",
" add:lang: Add a website language.\n",
+ " create:middleware: Create a new middleware class.\n",
From fab0c39acd3ae9f88a033e43122a7b0938880ca5 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 18:20:33 +0300
Subject: [PATCH 43/88] feat: Create Background Task Command
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateTaskCommand.php | 145 +++++++++++++
.../Tests/Cli/CreateTaskCommandTest.php | 201 ++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
4 files changed, 348 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateTaskCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index ed0bf9512..e1d7fd54d 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -449,6 +449,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\AddSmtpConnectionCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\AddLangCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateMiddlewareCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateTaskCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateTaskCommand.php b/WebFiori/Framework/Cli/Commands/CreateTaskCommand.php
new file mode 100644
index 000000000..0655d4e9b
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateTaskCommand.php
@@ -0,0 +1,145 @@
+getArgValue('--name');
+
+ if ($taskName === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Task name cannot be empty.');
+
+ $taskName = $this->getInput('Enter task name:', $className, $validator);
+ }
+ return $taskName;
+ }
+ private function getTaskDescription() : string {
+ $description = $this->getArgValue('--description');
+
+ if ($description === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Task description cannot be empty.');
+
+ $description = $this->getInput('Enter task description:', 'No Description', $validator);
+ }
+ return $description;
+ }
+ private function getTaskArguments() : array {
+ $args = [];
+ $argsJson = $this->getArgValue('--args');
+
+ if ($argsJson !== null) {
+ // Parse JSON arguments
+ $argsData = json_decode($argsJson, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->error('Invalid JSON format for --args parameter.');
+ return $args;
+ }
+
+ if (is_array($argsData)) {
+ foreach ($argsData as $argData) {
+ if (isset($argData['name'])) {
+ $taskArg = new TaskArgument(
+ $argData['name'],
+ $argData['description'] ?? 'No description'
+ );
+
+ if (isset($argData['default'])) {
+ $taskArg->setDefault($argData['default']);
+ }
+
+ $args[] = $taskArg;
+ }
+ }
+ }
+ } elseif ($this->getArgValue('--class-name') === null) {
+ // Only prompt if running interactively (no --class-name provided)
+ if ($this->confirm('Add execution arguments to the task?', false)) {
+ while (true) {
+ $argName = $this->getInput('Enter argument name (leave empty to finish):');
+ if (empty(trim($argName))) {
+ break;
+ }
+
+ $argDesc = $this->getInput('Enter argument description:', 'No description');
+ $argDefault = $this->getInput('Enter default value (leave empty for none):');
+
+ $taskArg = new TaskArgument(trim($argName), trim($argDesc));
+ if (!empty(trim($argDefault))) {
+ $taskArg->setDefault(trim($argDefault));
+ }
+
+ $args[] = $taskArg;
+ }
+ }
+ }
+
+ return $args;
+ }
+ /**
+ * Execute the command.
+ *
+ * @return int
+ */
+ public function exec() : int {
+ $className = $this->getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter task class name:', null, $validator);
+ }
+
+ $className = trim($className);
+
+ if (empty($className)) {
+ $this->error('Class name cannot be empty.');
+ return -1;
+ }
+
+ $taskName = $this->getTaskName($className);
+ $description = $this->getTaskDescription();
+ $args = $this->getTaskArguments();
+
+ $writer = new SchedulerTaskClassWriter($className, $taskName, $description, $args);
+ $writer->writeClass();
+
+ $this->success('Task class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php
new file mode 100644
index 000000000..bf10e9804
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php
@@ -0,0 +1,201 @@
+executeSingleCommand(new CreateTaskCommand(), [], [
+ $className,
+ "\n", // Use default task name (same as class name)
+ "\n", // Use default description
+ 'n' // Don't add arguments
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter task class name:\n",
+ "Enter task name: Enter = '$className'\n",
+ "Enter task description: Enter = 'No Description'\n",
+ "Add execution arguments to the task?(y/N)\n",
+ "Success: Task class created at: ".APP_PATH."Tasks".DIRECTORY_SEPARATOR.$className."Task.php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
+ $this->removeClass('\\App\\Tasks\\'.$className.'Task');
+ }
+ /**
+ * @test
+ */
+ public function testCreateTask01() {
+ $className = 'TestTask'.time();
+
+ $output = $this->executeSingleCommand(new CreateTaskCommand(), [], [
+ $className,
+ 'Email Sender Task',
+ 'Sends daily email reports',
+ 'y', // Add arguments
+ 'email',
+ 'Recipient email address',
+ 'admin@example.com',
+ 'subject',
+ 'Email subject',
+ "\n", // No default for subject
+ "\n" // Empty to finish adding arguments
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
+ $this->removeClass('\\App\\Tasks\\'.$className.'Task');
+ }
+ /**
+ * @test
+ */
+ public function testCreateTask02() {
+ $className = 'TestTask'.time();
+
+ $output = $this->executeSingleCommand(new CreateTaskCommand(), [], [
+ '', // Empty class name - will be rejected
+ $className, // Valid class name
+ "\n", // Use default task name
+ "\n", // Use default description
+ 'n' // No arguments
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter task class name:\n",
+ "Error: Class name cannot be empty.\n",
+ "Enter task class name:\n",
+ "Enter task name: Enter = '$className'\n",
+ "Enter task description: Enter = 'No Description'\n",
+ "Add execution arguments to the task?(y/N)\n",
+ "Success: Task class created at: ".APP_PATH."Tasks".DIRECTORY_SEPARATOR.$className."Task.php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
+ $this->removeClass('\\App\\Tasks\\'.$className.'Task');
+ }
+ /**
+ * @test
+ */
+ public function testCreateTask03() {
+ $className = 'TestTask'.time();
+
+ $output = $this->executeSingleCommand(new CreateTaskCommand(), [
+ 'WebFiori',
+ 'create:task'
+ ], [
+ $className,
+ 'Backup Task',
+ 'Creates database backup',
+ 'n'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
+ $this->removeClass('\\App\\Tasks\\'.$className.'Task');
+ }
+ /**
+ * @test
+ */
+ public function testCreateTaskWithArgs00() {
+ $className = 'TestTask'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateTaskCommand::class,
+ '--class-name' => $className,
+ '--name' => 'Cleanup Task',
+ '--description' => 'Cleans up temporary files'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Success: Task class created at: ".APP_PATH."Tasks".DIRECTORY_SEPARATOR.$className."Task.php\n", $output);
+
+ $this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
+ $this->removeClass('\\App\\Tasks\\'.$className.'Task');
+ }
+ /**
+ * @test
+ */
+ public function testCreateTaskWithArgs01() {
+ $className = 'TestTask'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateTaskCommand::class,
+ '--class-name' => $className,
+ '--name' => $className,
+ '--description' => 'Test task'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
+ $this->removeClass('\\App\\Tasks\\'.$className.'Task');
+ }
+ /**
+ * @test
+ */
+ public function testCreateTaskWithArgs02() {
+ $output = $this->executeMultiCommand([
+ CreateTaskCommand::class,
+ '--class-name' => '',
+ '--name' => 'Test',
+ '--description' => 'Test'
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Class name cannot be empty.\n", $output);
+ }
+ /**
+ * @test
+ */
+ public function testCreateTaskWithArgs03() {
+ $className = 'TestTask'.time();
+ $argsJson = json_encode([
+ ['name' => 'email', 'description' => 'Email address', 'default' => 'admin@example.com'],
+ ['name' => 'subject', 'description' => 'Email subject']
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateTaskCommand::class,
+ '--class-name' => $className,
+ '--name' => 'Email Task',
+ '--description' => 'Sends emails',
+ '--args' => $argsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
+ $this->removeClass('\\App\\Tasks\\'.$className.'Task');
+ }
+ /**
+ * @test
+ */
+ public function testCreateTaskWithArgs04() {
+ $className = 'TestTask'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateTaskCommand::class,
+ '--class-name' => $className,
+ '--name' => 'Simple Task',
+ '--description' => 'Does something',
+ '--args' => 'invalid-json'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Error: Invalid JSON format for --args parameter.\n", $output);
+ $this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
+ $this->removeClass('\\App\\Tasks\\'.$className.'Task');
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 1cd225d82..7014c31e1 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -25,6 +25,7 @@ public function test00() {
" add:smtp-connection: Add an SMTP account.\n",
" add:lang: Add a website language.\n",
" create:middleware: Create a new middleware class.\n",
+ " create:task: Create a new scheduler task class.\n",
From 3c88ecf2b547e9e55e652800e63d8c73fbe97dcf Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 18:24:26 +0300
Subject: [PATCH 44/88] feat: Create CLI Command
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateCommandCommand.php | 159 +++++++++++++++++
.../Tests/Cli/CreateCommandCommandTest.php | 163 ++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
4 files changed, 324 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateCommandCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index e1d7fd54d..5237be323 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -450,6 +450,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\AddLangCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateMiddlewareCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateTaskCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateCommandCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateCommandCommand.php b/WebFiori/Framework/Cli/Commands/CreateCommandCommand.php
new file mode 100644
index 000000000..641f95b09
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateCommandCommand.php
@@ -0,0 +1,159 @@
+getArgValue('--name');
+
+ if ($commandName === null) {
+ $validator = new InputValidator(function($input) {
+ $trimmed = trim($input);
+ return !empty($trimmed) && strpos($trimmed, ' ') === false;
+ }, 'Command name cannot be empty or contain spaces.');
+
+ $commandName = $this->getInput('Enter command name:', strtolower($className), $validator);
+ }
+ return $commandName;
+ }
+ private function getCommandDescription() : string {
+ $description = $this->getArgValue('--description');
+
+ if ($description === null) {
+ $description = $this->getInput('Enter command description:', '');
+ }
+ return $description;
+ }
+ private function getCommandArguments() : array {
+ $args = [];
+ $argsJson = $this->getArgValue('--args');
+
+ if ($argsJson !== null) {
+ $argsData = json_decode($argsJson, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->error('Invalid JSON format for --args parameter.');
+ return $args;
+ }
+
+ if (is_array($argsData)) {
+ foreach ($argsData as $argData) {
+ if (isset($argData['name'])) {
+ $arg = new Argument(
+ $argData['name'],
+ $argData['description'] ?? '',
+ $argData['optional'] ?? true
+ );
+
+ if (isset($argData['values']) && is_array($argData['values'])) {
+ foreach ($argData['values'] as $val) {
+ $arg->addAllowedValue($val);
+ }
+ }
+
+ $args[] = $arg;
+ }
+ }
+ }
+ } elseif ($this->getArgValue('--class-name') === null) {
+ if ($this->confirm('Add arguments to the command?', false)) {
+ while (true) {
+ $argName = $this->getInput('Enter argument name (leave empty to finish):');
+ if (empty(trim($argName))) {
+ break;
+ }
+
+ $argDesc = $this->getInput('Enter argument description:', '');
+ $isOptional = $this->confirm('Is this argument optional?', true);
+
+ $arg = new Argument(trim($argName), trim($argDesc), $isOptional);
+
+ if ($this->confirm('Add allowed values for this argument?', false)) {
+ while (true) {
+ $value = $this->getInput('Enter allowed value (leave empty to finish):');
+ if (empty(trim($value))) {
+ break;
+ }
+ $arg->addAllowedValue(trim($value));
+ }
+ }
+
+ $args[] = $arg;
+ }
+ }
+ }
+
+ return $args;
+ }
+ /**
+ * Execute the command.
+ *
+ * @return int
+ */
+ public function exec() : int {
+ $className = $this->getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter command class name:', null, $validator);
+ }
+
+ $className = trim($className);
+
+ if (empty($className)) {
+ $this->error('Class name cannot be empty.');
+ return -1;
+ }
+
+ $commandName = $this->getCommandName($className);
+
+ if (empty(trim($commandName)) || strpos($commandName, ' ') !== false) {
+ $this->error('Command name cannot be empty or contain spaces.');
+ return -1;
+ }
+
+ $description = $this->getCommandDescription();
+ $args = $this->getCommandArguments();
+
+ $writer = new CommandClassWriter();
+ $writer->setClassName($className);
+ $writer->setCommandName($commandName);
+ $writer->setCommandDescription($description);
+ $writer->setArgs($args);
+ $writer->writeClass();
+
+ $this->success('Command class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php
new file mode 100644
index 000000000..a9e3a95e4
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php
@@ -0,0 +1,163 @@
+executeSingleCommand(new CreateCommandCommand(), [], [
+ $className,
+ "\n", // Use default command name
+ "\n", // Use default description
+ 'n' // Don't add arguments
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter command class name:\n",
+ "Enter command name: Enter = '".strtolower($className)."'\n",
+ "Enter command description: Enter = ''\n",
+ "Add arguments to the command?(y/N)\n",
+ "Success: Command class created at: ".APP_PATH."Commands".DIRECTORY_SEPARATOR.$className."Command.php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Commands\\'.$className.'Command'));
+ $this->removeClass('\\App\\Commands\\'.$className.'Command');
+ }
+ /**
+ * @test
+ */
+ public function testCreateCommand01() {
+ $className = 'TestCmd'.time();
+
+ $output = $this->executeSingleCommand(new CreateCommandCommand(), [], [
+ $className,
+ 'test-command',
+ 'A test command',
+ 'y', // Add arguments
+ 'name',
+ 'User name',
+ 'n', // Not optional
+ 'n', // No allowed values
+ 'email',
+ 'User email',
+ 'y', // Optional
+ 'n', // No allowed values
+ "\n" // Empty to finish adding arguments
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Commands\\'.$className.'Command'));
+ $this->removeClass('\\App\\Commands\\'.$className.'Command');
+ }
+ /**
+ * @test
+ */
+ public function testCreateCommand02() {
+ $className = 'TestCmd'.time();
+
+ $output = $this->executeSingleCommand(new CreateCommandCommand(), [], [
+ '', // Empty class name - will be rejected
+ $className, // Valid class name
+ "\n", // Use default command name
+ "\n", // Use default description
+ 'n' // No arguments
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter command class name:\n",
+ "Error: Class name cannot be empty.\n",
+ "Enter command class name:\n",
+ "Enter command name: Enter = '".strtolower($className)."'\n",
+ "Enter command description: Enter = ''\n",
+ "Add arguments to the command?(y/N)\n",
+ "Success: Command class created at: ".APP_PATH."Commands".DIRECTORY_SEPARATOR.$className."Command.php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Commands\\'.$className.'Command'));
+ $this->removeClass('\\App\\Commands\\'.$className.'Command');
+ }
+ /**
+ * @test
+ */
+ public function testCreateCommandWithArgs00() {
+ $className = 'TestCmd'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateCommandCommand::class,
+ '--class-name' => $className,
+ '--name' => 'my-command',
+ '--description' => 'My custom command'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Success: Command class created at: ".APP_PATH."Commands".DIRECTORY_SEPARATOR.$className."Command.php\n", $output);
+
+ $this->assertTrue(class_exists('\\App\\Commands\\'.$className.'Command'));
+ $this->removeClass('\\App\\Commands\\'.$className.'Command');
+ }
+ /**
+ * @test
+ */
+ public function testCreateCommandWithArgs01() {
+ $className = 'TestCmd'.time();
+ $argsJson = json_encode([
+ ['name' => '--name', 'description' => 'User name', 'optional' => false],
+ ['name' => '--type', 'description' => 'User type', 'optional' => true, 'values' => ['admin', 'user']]
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateCommandCommand::class,
+ '--class-name' => $className,
+ '--name' => 'user-command',
+ '--description' => 'Manages users',
+ '--args' => $argsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Commands\\'.$className.'Command'));
+ $this->removeClass('\\App\\Commands\\'.$className.'Command');
+ }
+ /**
+ * @test
+ */
+ public function testCreateCommandWithArgs02() {
+ $output = $this->executeMultiCommand([
+ CreateCommandCommand::class,
+ '--class-name' => '',
+ '--name' => 'test',
+ '--description' => 'Test'
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Class name cannot be empty.\n", $output);
+ }
+ /**
+ * @test
+ */
+ public function testCreateCommandWithArgs03() {
+ $className = 'TestCmd'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateCommandCommand::class,
+ '--class-name' => $className,
+ '--name' => 'test command',
+ '--description' => 'Test'
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Command name cannot be empty or contain spaces.\n", $output);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 7014c31e1..f6fdcb74a 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -26,6 +26,7 @@ public function test00() {
" add:lang: Add a website language.\n",
" create:middleware: Create a new middleware class.\n",
" create:task: Create a new scheduler task class.\n",
+ " create:command: Create a new CLI command class.\n",
From 3c4944c4c4a0696784f6b1b1c7b47d4d7c38d56a Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 18:32:39 +0300
Subject: [PATCH 45/88] feat: Create Domain Entity
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateEntityCommand.php | 114 +++++++++++++
.../Framework/Writers/DomainEntityWriter.php | 2 +-
.../Tests/Cli/CreateEntityCommandTest.php | 150 ++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
5 files changed, 267 insertions(+), 1 deletion(-)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateEntityCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 5237be323..f5b12bf0c 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -451,6 +451,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\CreateMiddlewareCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateTaskCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateCommandCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateEntityCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateEntityCommand.php b/WebFiori/Framework/Cli/Commands/CreateEntityCommand.php
new file mode 100644
index 000000000..9f5c1e74f
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateEntityCommand.php
@@ -0,0 +1,114 @@
+getArgValue('--properties');
+
+ if ($propsJson !== null) {
+ $propsData = json_decode($propsJson, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->error('Invalid JSON format for --properties parameter.');
+ return $properties;
+ }
+
+ if (is_array($propsData)) {
+ foreach ($propsData as $propData) {
+ if (isset($propData['name']) && isset($propData['type'])) {
+ $properties[] = [
+ 'name' => $propData['name'],
+ 'type' => $propData['type'],
+ 'nullable' => $propData['nullable'] ?? false
+ ];
+ }
+ }
+ }
+ } elseif ($this->getArgValue('--class-name') === null) {
+ if ($this->confirm('Add properties to the entity?', false)) {
+ while (true) {
+ $propName = $this->getInput('Enter property name (leave empty to finish):');
+ if (empty(trim($propName))) {
+ break;
+ }
+
+ $propType = $this->getInput('Enter property type:', 'string');
+ $nullable = $this->confirm('Is this property nullable?', false);
+
+ $properties[] = [
+ 'name' => trim($propName),
+ 'type' => trim($propType),
+ 'nullable' => $nullable
+ ];
+ }
+ }
+ }
+
+ return $properties;
+ }
+ /**
+ * Execute the command.
+ *
+ * @return int
+ */
+ public function exec() : int {
+ $className = $this->getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter entity class name:', null, $validator);
+ }
+
+ $className = trim($className);
+
+ if (empty($className)) {
+ $this->error('Class name cannot be empty.');
+ return -1;
+ }
+
+ $properties = $this->getEntityProperties();
+
+ $writer = new DomainEntityWriter();
+ $writer->setClassName($className);
+
+ foreach ($properties as $prop) {
+ $writer->addProperty($prop['name'], $prop['type'], $prop['nullable']);
+ }
+
+ $writer->writeClass();
+
+ $this->success('Entity class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/WebFiori/Framework/Writers/DomainEntityWriter.php b/WebFiori/Framework/Writers/DomainEntityWriter.php
index cc0b7f2b4..346b10d0a 100644
--- a/WebFiori/Framework/Writers/DomainEntityWriter.php
+++ b/WebFiori/Framework/Writers/DomainEntityWriter.php
@@ -47,7 +47,7 @@ public function writeClassDeclaration() {
$this->append('class '.$this->getName().' {');
}
- private function writeConstructor() {
+ protected function writeConstructor(array $params = [], $body = '', string $description = 'Creates new instance of the class.', int $indent = 1) {
$this->append('public function __construct(', 1);
$params = [];
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php
new file mode 100644
index 000000000..9884b55d9
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php
@@ -0,0 +1,150 @@
+executeSingleCommand(new CreateEntityCommand(), [], [
+ $className,
+ 'n' // Don't add properties
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter entity class name:\n",
+ "Add properties to the entity?(y/N)\n",
+ "Success: Entity class created at: ".APP_PATH."Domain".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Domain\\'.$className));
+ $this->removeClass('\\App\\Domain\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateEntity01() {
+ $className = 'TestEntity'.time();
+
+ $output = $this->executeSingleCommand(new CreateEntityCommand(), [], [
+ $className,
+ 'y', // Add properties
+ 'id',
+ 'int',
+ 'n', // Not nullable
+ 'name',
+ 'string',
+ 'n', // Not nullable
+ 'email',
+ 'string',
+ 'y', // Nullable
+ "\n" // Empty to finish adding properties
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Domain\\'.$className));
+ $this->removeClass('\\App\\Domain\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateEntity02() {
+ $className = 'TestEntity'.time();
+
+ $output = $this->executeSingleCommand(new CreateEntityCommand(), [], [
+ '', // Empty class name - will be rejected
+ $className, // Valid class name
+ 'n' // No properties
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter entity class name:\n",
+ "Error: Class name cannot be empty.\n",
+ "Enter entity class name:\n",
+ "Add properties to the entity?(y/N)\n",
+ "Success: Entity class created at: ".APP_PATH."Domain".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Domain\\'.$className));
+ $this->removeClass('\\App\\Domain\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateEntityWithArgs00() {
+ $className = 'TestEntity'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateEntityCommand::class,
+ '--class-name' => $className
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Success: Entity class created at: ".APP_PATH."Domain".DIRECTORY_SEPARATOR.$className.".php\n", $output);
+
+ $this->assertTrue(class_exists('\\App\\Domain\\'.$className));
+ $this->removeClass('\\App\\Domain\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateEntityWithArgs01() {
+ $className = 'TestEntity'.time();
+ $propsJson = json_encode([
+ ['name' => 'id', 'type' => 'int', 'nullable' => false],
+ ['name' => 'name', 'type' => 'string', 'nullable' => false],
+ ['name' => 'email', 'type' => 'string', 'nullable' => true]
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateEntityCommand::class,
+ '--class-name' => $className,
+ '--properties' => $propsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Domain\\'.$className));
+ $this->removeClass('\\App\\Domain\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateEntityWithArgs02() {
+ $output = $this->executeMultiCommand([
+ CreateEntityCommand::class,
+ '--class-name' => ''
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Class name cannot be empty.\n", $output);
+ }
+ /**
+ * @test
+ */
+ public function testCreateEntityWithArgs03() {
+ $className = 'TestEntity'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateEntityCommand::class,
+ '--class-name' => $className,
+ '--properties' => 'invalid-json'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Error: Invalid JSON format for --properties parameter.\n", $output);
+ $this->assertTrue(class_exists('\\App\\Domain\\'.$className));
+ $this->removeClass('\\App\\Domain\\'.$className);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index f6fdcb74a..e3193f39d 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -27,6 +27,7 @@ public function test00() {
" create:middleware: Create a new middleware class.\n",
" create:task: Create a new scheduler task class.\n",
" create:command: Create a new CLI command class.\n",
+ " create:entity: Create a new domain entity class.\n",
From ff39f02ddadcfcbef0bccd9d3c3dc38fec1c49eb Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 18:41:49 +0300
Subject: [PATCH 46/88] feat: Web Services Writer
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateServiceCommand.php | 157 ++++++++++++++++++
.../Tests/Cli/CreateServiceCommandTest.php | 147 ++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
4 files changed, 306 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateServiceCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index f5b12bf0c..8ef84146e 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -452,6 +452,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\CreateTaskCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateCommandCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateEntityCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateServiceCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateServiceCommand.php b/WebFiori/Framework/Cli/Commands/CreateServiceCommand.php
new file mode 100644
index 000000000..3182c46a0
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateServiceCommand.php
@@ -0,0 +1,157 @@
+getArgValue('--description');
+
+ if ($description === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Service description cannot be empty.');
+
+ $description = $this->getInput('Enter service description:', 'REST API Service', $validator);
+ }
+ return $description;
+ }
+ private function getServiceMethods() : array {
+ $methods = [];
+ $methodsJson = $this->getArgValue('--methods');
+
+ if ($methodsJson !== null) {
+ $methodsData = json_decode($methodsJson, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->error('Invalid JSON format for --methods parameter.');
+ return $methods;
+ }
+
+ if (is_array($methodsData)) {
+ $methods = $methodsData;
+ }
+ } elseif ($this->getArgValue('--class-name') === null) {
+ if ($this->confirm('Add methods to the service?', false)) {
+ while (true) {
+ $methodName = $this->getInput('Enter method name (leave empty to finish):');
+ if (empty(trim($methodName))) {
+ break;
+ }
+
+ $httpMethod = $this->select('Select HTTP method:', RequestMethod::getAll());
+ $returnType = $this->getInput('Enter return type:', 'array');
+
+ $params = [];
+ if ($this->confirm('Add parameters to this method?', false)) {
+ while (true) {
+ $paramName = $this->getInput('Enter parameter name (leave empty to finish):');
+ if (empty(trim($paramName))) {
+ break;
+ }
+
+ $paramType = $this->select('Select parameter type:', ParamType::getTypes());
+ $paramDesc = $this->getInput('Enter parameter description:', '');
+
+ $param = [
+ 'name' => trim($paramName),
+ 'type' => $paramType,
+ 'description' => trim($paramDesc)
+ ];
+
+ if (in_array($paramType, [ParamType::INT, ParamType::DOUBLE])) {
+ if ($this->confirm('Add min/max constraints?', false)) {
+ $param['min'] = (int)$this->getInput('Enter minimum value:');
+ $param['max'] = (int)$this->getInput('Enter maximum value:');
+ }
+ }
+
+ $params[] = $param;
+ }
+ }
+
+ $methods[] = [
+ 'http' => $httpMethod,
+ 'name' => trim($methodName),
+ 'params' => $params,
+ 'return' => trim($returnType)
+ ];
+ }
+ }
+ }
+
+ return $methods;
+ }
+ /**
+ * Execute the command.
+ *
+ * @return int
+ */
+ public function exec() : int {
+ $className = $this->getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter service class name:', null, $validator);
+ }
+
+ $className = trim($className);
+
+ if (empty($className)) {
+ $this->error('Class name cannot be empty.');
+ return -1;
+ }
+
+ $description = $this->getServiceDescription();
+ $methods = $this->getServiceMethods();
+
+ $writer = new RestServiceWriter();
+ $writer->setClassName($className);
+ $writer->setDescription($description);
+
+ foreach ($methods as $method) {
+ $writer->addMethod(
+ $method['http'],
+ $method['name'],
+ $method['params'] ?? [],
+ $method['return'] ?? 'array'
+ );
+ }
+
+ $writer->writeClass();
+
+ $this->success('Service class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php
new file mode 100644
index 000000000..1fc9f4fe0
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php
@@ -0,0 +1,147 @@
+executeSingleCommand(new CreateServiceCommand(), [], [
+ $className,
+ "\n", // Use default description
+ 'n' // Don't add methods
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter service class name:\n",
+ "Enter service description: Enter = 'REST API Service'\n",
+ "Add methods to the service?(y/N)\n",
+ "Success: Service class created at: ".APP_PATH."Apis".DIRECTORY_SEPARATOR.$className."Service.php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Apis\\'.$className.'Service'));
+ $this->removeClass('\\App\\Apis\\'.$className.'Service');
+ }
+ /**
+ * @test
+ */
+ public function testCreateService01() {
+ $className = 'TestService'.time();
+
+ $output = $this->executeSingleCommand(new CreateServiceCommand(), [], [
+ '', // Empty class name - will be rejected
+ $className, // Valid class name
+ "\n", // Use default description
+ 'n' // No methods
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter service class name:\n",
+ "Error: Class name cannot be empty.\n",
+ "Enter service class name:\n",
+ "Enter service description: Enter = 'REST API Service'\n",
+ "Add methods to the service?(y/N)\n",
+ "Success: Service class created at: ".APP_PATH."Apis".DIRECTORY_SEPARATOR.$className."Service.php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Apis\\'.$className.'Service'));
+ $this->removeClass('\\App\\Apis\\'.$className.'Service');
+ }
+ /**
+ * @test
+ */
+ public function testCreateServiceWithArgs00() {
+ $className = 'TestService'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateServiceCommand::class,
+ '--class-name' => $className,
+ '--description' => 'User management service'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Success: Service class created at: ".APP_PATH."Apis".DIRECTORY_SEPARATOR.$className."Service.php\n", $output);
+
+ $this->assertTrue(class_exists('\\App\\Apis\\'.$className.'Service'));
+ $this->removeClass('\\App\\Apis\\'.$className.'Service');
+ }
+ /**
+ * @test
+ */
+ public function testCreateServiceWithArgs01() {
+ $className = 'TestService'.time();
+ $methodsJson = json_encode([
+ [
+ 'http' => 'GET',
+ 'name' => 'getUser',
+ 'params' => [
+ ['name' => 'id', 'type' => 'INT', 'description' => 'User ID', 'min' => 1]
+ ],
+ 'return' => 'array'
+ ],
+ [
+ 'http' => 'POST',
+ 'name' => 'createUser',
+ 'params' => [
+ ['name' => 'name', 'type' => 'STRING', 'description' => 'User name'],
+ ['name' => 'email', 'type' => 'EMAIL', 'description' => 'User email']
+ ],
+ 'return' => 'array'
+ ]
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateServiceCommand::class,
+ '--class-name' => $className,
+ '--description' => 'User API',
+ '--methods' => $methodsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Apis\\'.$className.'Service'));
+ $this->removeClass('\\App\\Apis\\'.$className.'Service');
+ }
+ /**
+ * @test
+ */
+ public function testCreateServiceWithArgs02() {
+ $output = $this->executeMultiCommand([
+ CreateServiceCommand::class,
+ '--class-name' => '',
+ '--description' => 'Test'
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Class name cannot be empty.\n", $output);
+ }
+ /**
+ * @test
+ */
+ public function testCreateServiceWithArgs03() {
+ $className = 'TestService'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateServiceCommand::class,
+ '--class-name' => $className,
+ '--description' => 'Test service',
+ '--methods' => 'invalid-json'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Error: Invalid JSON format for --methods parameter.\n", $output);
+ $this->assertTrue(class_exists('\\App\\Apis\\'.$className.'Service'));
+ $this->removeClass('\\App\\Apis\\'.$className.'Service');
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index e3193f39d..ae86490df 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -28,6 +28,7 @@ public function test00() {
" create:task: Create a new scheduler task class.\n",
" create:command: Create a new CLI command class.\n",
" create:entity: Create a new domain entity class.\n",
+ " create:service: Create a new REST service class.\n",
From 6fe3be5486c6f98ec8409a644ad15d8ec5f96cd0 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 18:47:43 +0300
Subject: [PATCH 47/88] feat: Create Table Command
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateTableCommand.php | 153 ++++++++++++++++++
.../Tests/Cli/CreateTableCommandTest.php | 133 +++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
4 files changed, 288 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateTableCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 8ef84146e..36ab0e67e 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -453,6 +453,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\CreateCommandCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateEntityCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateServiceCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateTableCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateTableCommand.php b/WebFiori/Framework/Cli/Commands/CreateTableCommand.php
new file mode 100644
index 000000000..017508b8d
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateTableCommand.php
@@ -0,0 +1,153 @@
+getArgValue('--table-name');
+
+ if ($tableName === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Table name cannot be empty.');
+
+ $tableName = $this->getInput('Enter table name:', strtolower($className), $validator);
+ }
+ return $tableName;
+ }
+ private function getTableColumns() : array {
+ $columns = [];
+ $columnsJson = $this->getArgValue('--columns');
+
+ if ($columnsJson !== null) {
+ $columnsData = json_decode($columnsJson, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->error('Invalid JSON format for --columns parameter.');
+ return $columns;
+ }
+
+ if (is_array($columnsData)) {
+ $columns = $columnsData;
+ }
+ } elseif ($this->getArgValue('--class-name') === null) {
+ if ($this->confirm('Add columns to the table?', false)) {
+ while (true) {
+ $colName = $this->getInput('Enter column name (leave empty to finish):');
+ if (empty(trim($colName))) {
+ break;
+ }
+
+ $colType = $this->select('Select column type:', DataType::getTypes());
+
+ $column = [
+ 'name' => trim($colName),
+ 'type' => $colType
+ ];
+
+ if (in_array($colType, [DataType::VARCHAR, DataType::TEXT, DataType::INT])) {
+ $size = $this->getInput('Enter column size (leave empty for default):');
+ if (!empty(trim($size))) {
+ $column['size'] = (int)$size;
+ }
+ }
+
+ if ($this->confirm('Is this a primary key?', false)) {
+ $column['primary'] = true;
+
+ if ($colType === DataType::INT) {
+ $column['autoIncrement'] = $this->confirm('Auto increment?', true);
+ }
+ }
+
+ $column['nullable'] = $this->confirm('Is this column nullable?', false);
+
+ $columns[] = $column;
+ }
+ }
+ }
+
+ return $columns;
+ }
+ /**
+ * Execute the command.
+ *
+ * @return int
+ */
+ public function exec() : int {
+ $className = $this->getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter table class name:', null, $validator);
+ }
+
+ $className = trim($className);
+
+ if (empty($className)) {
+ $this->error('Class name cannot be empty.');
+ return -1;
+ }
+
+ $tableName = $this->getTableName($className);
+ $columns = $this->getTableColumns();
+
+ $writer = new AttributeTableWriter();
+ $writer->setClassName($className);
+ $writer->setTableName($tableName);
+
+ foreach ($columns as $col) {
+ $options = [];
+ if (isset($col['size'])) {
+ $options['size'] = $col['size'];
+ }
+ if (isset($col['primary'])) {
+ $options['primary'] = $col['primary'];
+ }
+ if (isset($col['autoIncrement'])) {
+ $options['autoIncrement'] = $col['autoIncrement'];
+ }
+ if (isset($col['nullable'])) {
+ $options['nullable'] = $col['nullable'];
+ }
+
+ $writer->addColumn($col['name'], $col['type'], $options);
+ }
+
+ $writer->writeClass();
+
+ $this->success('Table class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
new file mode 100644
index 000000000..4e73c0877
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
@@ -0,0 +1,133 @@
+executeSingleCommand(new CreateTableCommand(), [], [
+ $className,
+ "\n", // Use default table name
+ 'n' // Don't add columns
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter table class name:\n",
+ "Enter table name: Enter = '".strtolower($className)."'\n",
+ "Add columns to the table?(y/N)\n",
+ "Success: Table class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Schema".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateTable01() {
+ $className = 'TestTable'.time();
+
+ $output = $this->executeSingleCommand(new CreateTableCommand(), [], [
+ '', // Empty class name - will be rejected
+ $className, // Valid class name
+ "\n", // Use default table name
+ 'n' // No columns
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter table class name:\n",
+ "Error: Class name cannot be empty.\n",
+ "Enter table class name:\n",
+ "Enter table name: Enter = '".strtolower($className)."'\n",
+ "Add columns to the table?(y/N)\n",
+ "Success: Table class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Schema".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateTableWithArgs00() {
+ $className = 'TestTable'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateTableCommand::class,
+ '--class-name' => $className,
+ '--table-name' => 'users'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Success: Table class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Schema".DIRECTORY_SEPARATOR.$className.".php\n", $output);
+
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateTableWithArgs01() {
+ $className = 'TestTable'.time();
+ $columnsJson = json_encode([
+ ['name' => 'id', 'type' => 'INT', 'size' => 11, 'primary' => true, 'autoIncrement' => true],
+ ['name' => 'name', 'type' => 'VARCHAR', 'size' => 255, 'nullable' => false],
+ ['name' => 'email', 'type' => 'VARCHAR', 'size' => 255, 'nullable' => true]
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateTableCommand::class,
+ '--class-name' => $className,
+ '--table-name' => 'users',
+ '--columns' => $columnsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateTableWithArgs02() {
+ $output = $this->executeMultiCommand([
+ CreateTableCommand::class,
+ '--class-name' => '',
+ '--table-name' => 'test'
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Class name cannot be empty.\n", $output);
+ }
+ /**
+ * @test
+ */
+ public function testCreateTableWithArgs03() {
+ $className = 'TestTable'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateTableCommand::class,
+ '--class-name' => $className,
+ '--table-name' => 'test_table',
+ '--columns' => 'invalid-json'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Error: Invalid JSON format for --columns parameter.\n", $output);
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index ae86490df..3845afe47 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -29,6 +29,7 @@ public function test00() {
" create:command: Create a new CLI command class.\n",
" create:entity: Create a new domain entity class.\n",
" create:service: Create a new REST service class.\n",
+ " create:table: Create a new database table schema class.\n",
From 498c2141fd48ed23acc79e9cf08e3bfc73265fd9 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 19:02:08 +0300
Subject: [PATCH 48/88] feat: Create Repo
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateRepositoryCommand.php | 145 +++++++++++++++++
.../Framework/Writers/RepositoryWriter.php | 8 +-
.../Tests/Cli/CreateRepositoryCommandTest.php | 148 ++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
5 files changed, 299 insertions(+), 4 deletions(-)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateRepositoryCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 36ab0e67e..0f5386960 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -454,6 +454,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\CreateEntityCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateServiceCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateTableCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateRepositoryCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateRepositoryCommand.php b/WebFiori/Framework/Cli/Commands/CreateRepositoryCommand.php
new file mode 100644
index 000000000..86f047d4a
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateRepositoryCommand.php
@@ -0,0 +1,145 @@
+getArgValue('--entity');
+
+ if ($entity === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Entity class cannot be empty.');
+
+ $entity = $this->getInput('Enter entity class (e.g., App\\Domain\\User):', null, $validator);
+ }
+ return $entity;
+ }
+ private function getTableName() : string {
+ $table = $this->getArgValue('--table');
+
+ if ($table === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Table name cannot be empty.');
+
+ $table = $this->getInput('Enter table name:', null, $validator);
+ }
+ return $table;
+ }
+ private function getIdField() : string {
+ $idField = $this->getArgValue('--id-field');
+
+ if ($idField === null) {
+ $idField = $this->getInput('Enter ID field name:', 'id');
+ }
+ return $idField;
+ }
+ private function getProperties() : array {
+ $properties = [];
+ $propsJson = $this->getArgValue('--properties');
+
+ if ($propsJson !== null) {
+ $propsData = json_decode($propsJson, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->error('Invalid JSON format for --properties parameter.');
+ return $properties;
+ }
+
+ if (is_array($propsData)) {
+ $properties = $propsData;
+ }
+ } elseif ($this->getArgValue('--class-name') === null) {
+ if ($this->confirm('Add properties to the repository?', false)) {
+ while (true) {
+ $propName = $this->getInput('Enter property name (leave empty to finish):');
+ if (empty(trim($propName))) {
+ break;
+ }
+
+ $propType = $this->select('Select property type:', ['string', 'int', 'float', 'bool', 'array']);
+
+ $properties[] = [
+ 'name' => trim($propName),
+ 'type' => $propType
+ ];
+ }
+ }
+ }
+
+ return $properties;
+ }
+ /**
+ * Execute the command.
+ *
+ * @return int
+ */
+ public function exec() : int {
+ $className = $this->getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter repository class name:', null, $validator);
+ }
+
+ $className = trim($className);
+
+ if (empty($className)) {
+ $this->error('Class name cannot be empty.');
+ return -1;
+ }
+
+ $entityClass = $this->getEntityClass();
+ $tableName = $this->getTableName();
+ $idField = $this->getIdField();
+ $properties = $this->getProperties();
+
+ $writer = new RepositoryWriter();
+ $writer->setClassName($className);
+ $writer->setEntityClass($entityClass);
+ $writer->setTableName($tableName);
+ $writer->setIdField($idField);
+
+ foreach ($properties as $prop) {
+ $writer->addProperty($prop['name'], $prop['type']);
+ }
+
+ $writer->writeClass();
+
+ $this->success('Repository class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/WebFiori/Framework/Writers/RepositoryWriter.php b/WebFiori/Framework/Writers/RepositoryWriter.php
index 85611659c..0056d48ed 100644
--- a/WebFiori/Framework/Writers/RepositoryWriter.php
+++ b/WebFiori/Framework/Writers/RepositoryWriter.php
@@ -66,14 +66,14 @@ public function writeClassDeclaration() {
}
private function writeGetTableName() {
- $this->append($this->f('getTableName', [], 'string'), 1);
+ $this->f('getTableName', [], 'string');
$this->append('return \''.$this->tableName.'\';', 2);
$this->append('}', 1);
$this->append('', 1);
}
private function writeGetIdField() {
- $this->append($this->f('getIdField', [], 'string'), 1);
+ $this->f('getIdField', [], 'string');
$this->append('return \''.$this->idField.'\';', 2);
$this->append('}', 1);
$this->append('', 1);
@@ -81,7 +81,7 @@ private function writeGetIdField() {
private function writeToEntity() {
$entityShortName = basename(str_replace('\\', '/', $this->entityClass));
- $this->append($this->f('toEntity', ['row' => 'array'], $entityShortName), 1);
+ $this->f('toEntity', ['row' => 'array'], 'object');
$this->append('return new '.$entityShortName.'(', 2);
$params = [];
@@ -97,7 +97,7 @@ private function writeToEntity() {
}
private function writeToArray() {
- $this->append($this->f('toArray', ['entity' => 'object'], 'array'), 1);
+ $this->f('toArray', ['entity' => 'object'], 'array');
$this->append('return [', 2);
foreach ($this->properties as $prop) {
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php
new file mode 100644
index 000000000..b9a63ff79
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php
@@ -0,0 +1,148 @@
+executeSingleCommand(new CreateRepositoryCommand(), [], [
+ $className,
+ 'App\\Domain\\User',
+ 'users',
+ "\n", // Use default id field
+ 'n' // Don't add properties
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter repository class name:\n",
+ "Enter entity class (e.g., App\\Domain\\User):\n",
+ "Enter table name:\n",
+ "Enter ID field name: Enter = 'id'\n",
+ "Add properties to the repository?(y/N)\n",
+ "Success: Repository class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Repository".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Repository\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Repository\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateRepository01() {
+ $className = 'TestRepo'.time();
+
+ $output = $this->executeSingleCommand(new CreateRepositoryCommand(), [], [
+ '', // Empty class name - will be rejected
+ $className, // Valid class name
+ 'App\\Domain\\User',
+ 'users',
+ "\n", // Use default id field
+ 'n' // No properties
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Enter repository class name:\n",
+ "Error: Class name cannot be empty.\n",
+ "Enter repository class name:\n",
+ "Enter entity class (e.g., App\\Domain\\User):\n",
+ "Enter table name:\n",
+ "Enter ID field name: Enter = 'id'\n",
+ "Add properties to the repository?(y/N)\n",
+ "Success: Repository class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Repository".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Repository\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Repository\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateRepositoryWithArgs00() {
+ $className = 'TestRepo'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateRepositoryCommand::class,
+ '--class-name' => $className,
+ '--entity' => 'App\\Domain\\User',
+ '--table' => 'users',
+ '--id-field' => 'id'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Success: Repository class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Repository".DIRECTORY_SEPARATOR.$className.".php\n", $output);
+
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Repository\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Repository\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateRepositoryWithArgs01() {
+ $className = 'TestRepo'.time();
+ $propsJson = json_encode([
+ ['name' => 'id', 'type' => 'int'],
+ ['name' => 'name', 'type' => 'string'],
+ ['name' => 'email', 'type' => 'string']
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateRepositoryCommand::class,
+ '--class-name' => $className,
+ '--entity' => 'App\\Domain\\User',
+ '--table' => 'users',
+ '--id-field' => 'id',
+ '--properties' => $propsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Repository\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Repository\\'.$className);
+ }
+ /**
+ * @test
+ */
+ public function testCreateRepositoryWithArgs02() {
+ $output = $this->executeMultiCommand([
+ CreateRepositoryCommand::class,
+ '--class-name' => '',
+ '--entity' => 'App\\Domain\\User',
+ '--table' => 'users'
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertContains("Error: Class name cannot be empty.\n", $output);
+ }
+ /**
+ * @test
+ */
+ public function testCreateRepositoryWithArgs03() {
+ $className = 'TestRepo'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateRepositoryCommand::class,
+ '--class-name' => $className,
+ '--entity' => 'App\\Domain\\User',
+ '--table' => 'users',
+ '--id-field' => 'id',
+ '--properties' => 'invalid-json'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertContains("Error: Invalid JSON format for --properties parameter.\n", $output);
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Repository\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Repository\\'.$className);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 3845afe47..589855e92 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -30,6 +30,7 @@ public function test00() {
" create:entity: Create a new domain entity class.\n",
" create:service: Create a new REST service class.\n",
" create:table: Create a new database table schema class.\n",
+ " create:repository: Create a new repository class.\n",
From 78d5ecf1986e4050ab70709f304816afabc18433 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 29 Jan 2026 19:06:57 +0300
Subject: [PATCH 49/88] feat: Create CRUD Resource
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateResourceCommand.php | 230 ++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
3 files changed, 232 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateResourceCommand.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 0f5386960..005ae1e2b 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -455,6 +455,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\CreateServiceCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateTableCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateRepositoryCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateResourceCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateResourceCommand.php b/WebFiori/Framework/Cli/Commands/CreateResourceCommand.php
new file mode 100644
index 000000000..da0571576
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateResourceCommand.php
@@ -0,0 +1,230 @@
+getArgValue('--name');
+
+ if ($name === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input)) && ctype_upper($input[0]);
+ }, 'Resource name cannot be empty and must start with uppercase letter.');
+
+ $name = $this->getInput('Enter resource name (e.g., User, Product):', null, $validator);
+ }
+ return trim($name);
+ }
+
+ private function getTableName(string $resourceName) : string {
+ $table = $this->getArgValue('--table');
+
+ if ($table === null) {
+ $default = strtolower($resourceName) . 's';
+ $table = $this->getInput('Enter table name:', $default);
+ }
+ return trim($table);
+ }
+
+ private function getIdField() : string {
+ $idField = $this->getArgValue('--id-field');
+
+ if ($idField === null) {
+ $idField = $this->getInput('Enter ID field name:', 'id');
+ }
+ return trim($idField);
+ }
+
+ private function getProperties() : array {
+ $properties = [];
+ $propsJson = $this->getArgValue('--properties');
+
+ if ($propsJson !== null) {
+ $propsData = json_decode($propsJson, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ $this->error('Invalid JSON format for --properties parameter.');
+ return $properties;
+ }
+
+ if (is_array($propsData)) {
+ $properties = $propsData;
+ }
+ } elseif ($this->getArgValue('--name') === null) {
+ $this->println('Add properties to the resource:');
+
+ while (true) {
+ $propName = $this->getInput('Enter property name (leave empty to finish):');
+ if (empty(trim($propName))) {
+ break;
+ }
+
+ $propType = $this->select('Select property type:', ['int', 'string', 'float', 'bool']);
+ $nullable = $this->confirm('Is this property nullable?', false);
+ $isPrimary = $this->confirm('Is this a primary key?', false);
+
+ $property = [
+ 'name' => trim($propName),
+ 'type' => $propType,
+ 'nullable' => $nullable,
+ 'primary' => $isPrimary
+ ];
+
+ if ($isPrimary && $propType === 'int') {
+ $property['autoIncrement'] = $this->confirm('Auto increment?', true);
+ }
+
+ if ($propType === 'string') {
+ $size = $this->getInput('Enter string size:', '255');
+ $property['size'] = (int)$size;
+ }
+
+ $properties[] = $property;
+ }
+ }
+
+ return $properties;
+ }
+
+ private function mapTypeToDataType(string $type) : string {
+ return match($type) {
+ 'int' => DataType::INT,
+ 'string' => DataType::VARCHAR,
+ 'float' => DataType::DOUBLE,
+ 'bool' => DataType::BOOL,
+ default => DataType::VARCHAR
+ };
+ }
+
+ /**
+ * Execute the command.
+ *
+ * @return int
+ */
+ public function exec() : int {
+ $resourceName = $this->getResourceName();
+ $tableName = $this->getTableName($resourceName);
+ $idField = $this->getIdField();
+ $properties = $this->getProperties();
+
+ if (empty($properties)) {
+ $this->error('At least one property is required.');
+ return -1;
+ }
+
+ // 1. Create Domain Entity
+ $entityWriter = new DomainEntityWriter();
+ $entityWriter->setClassName($resourceName);
+ foreach ($properties as $prop) {
+ $entityWriter->addProperty($prop['name'], $prop['type'], $prop['nullable'] ?? false);
+ }
+ $entityWriter->writeClass();
+ $this->success('✓ Created entity: '.$entityWriter->getAbsolutePath());
+
+ // 2. Create Table Schema
+ $tableWriter = new AttributeTableWriter();
+ $tableWriter->setClassName($resourceName.'Table');
+ $tableWriter->setTableName($tableName);
+ foreach ($properties as $prop) {
+ $options = [];
+ if (isset($prop['size'])) {
+ $options['size'] = $prop['size'];
+ }
+ if (isset($prop['primary']) && $prop['primary']) {
+ $options['primary'] = true;
+ }
+ if (isset($prop['autoIncrement']) && $prop['autoIncrement']) {
+ $options['autoIncrement'] = true;
+ }
+ if (isset($prop['nullable']) && $prop['nullable']) {
+ $options['nullable'] = true;
+ }
+
+ $tableWriter->addColumn($prop['name'], $this->mapTypeToDataType($prop['type']), $options);
+ }
+ $tableWriter->writeClass();
+ $this->success('✓ Created table: '.$tableWriter->getAbsolutePath());
+
+ // 3. Create Repository
+ $repoWriter = new RepositoryWriter();
+ $repoWriter->setClassName($resourceName.'Repository');
+ $repoWriter->setEntityClass(APP_DIR.'\\Domain\\'.$resourceName);
+ $repoWriter->setTableName($tableName);
+ $repoWriter->setIdField($idField);
+ foreach ($properties as $prop) {
+ $repoWriter->addProperty($prop['name'], $prop['type']);
+ }
+ $repoWriter->writeClass();
+ $this->success('✓ Created repository: '.$repoWriter->getAbsolutePath());
+
+ // 4. Create REST Service
+ $serviceWriter = new RestServiceWriter();
+ $serviceWriter->setClassName($resourceName.'Service');
+ $serviceWriter->setDescription($resourceName.' management API');
+
+ // Add CRUD methods
+ $idParam = [['name' => 'id', 'type' => 'INT', 'description' => $resourceName.' ID']];
+
+ $serviceWriter->addMethod('GET', 'get'.$resourceName, $idParam, 'array');
+ $serviceWriter->addMethod('GET', 'getAll'.ucfirst($tableName), [], 'array');
+
+ $createParams = [];
+ foreach ($properties as $prop) {
+ if (!($prop['primary'] ?? false) || !($prop['autoIncrement'] ?? false)) {
+ $createParams[] = [
+ 'name' => $prop['name'],
+ 'type' => strtoupper($prop['type']),
+ 'description' => ucfirst($prop['name'])
+ ];
+ }
+ }
+ $serviceWriter->addMethod('POST', 'create'.$resourceName, $createParams, 'array');
+ $serviceWriter->addMethod('PUT', 'update'.$resourceName, array_merge($idParam, $createParams), 'array');
+ $serviceWriter->addMethod('DELETE', 'delete'.$resourceName, $idParam, 'array');
+
+ $serviceWriter->writeClass();
+ $this->success('✓ Created service: '.$serviceWriter->getAbsolutePath());
+
+ $this->println('');
+ $this->println('Resource created successfully!');
+ $this->println('');
+ $this->println('Next steps:');
+ $this->println('1. Run migrations to create the database table');
+ $this->println('2. Implement business logic in the service methods');
+ $this->println('3. Access API at: /api/'.strtolower($resourceName));
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 589855e92..1a1b00abf 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -31,6 +31,7 @@ public function test00() {
" create:service: Create a new REST service class.\n",
" create:table: Create a new database table schema class.\n",
" create:repository: Create a new repository class.\n",
+ " create:resource: Create a complete CRUD resource (entity, table, repository, service).\n",
From 4f87f0d07e47e58c2dfa831eb41b7a202f079c88 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 3 Feb 2026 23:54:15 +0300
Subject: [PATCH 50/88] chore: Added CS Fixer
---
composer.json | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/composer.json b/composer.json
index 057e01a32..70b113ede 100644
--- a/composer.json
+++ b/composer.json
@@ -35,10 +35,12 @@
},
"scripts": {
"test": "phpunit --configuration tests/phpunit.xml",
- "test10": "phpunit --configuration tests/phpunit10.xml"
+ "test10": "phpunit --configuration tests/phpunit10.xml",
+ "fix-cs": "php-cs-fixer fix --config=php_cs.php.dist"
},
"require-dev": {
- "phpunit/phpunit": "^10.0"
+ "phpunit/phpunit": "^10.0",
+ "friendsofphp/php-cs-fixer": "^3.0"
},
"autoload": {
"psr-4": {
From 78f8c36b8864d8cbb56ab00a2532dc28de461718 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 3 Feb 2026 23:58:56 +0300
Subject: [PATCH 51/88] refactor: Enhanced Starter Page
---
WebFiori/Framework/Ui/StarterPage.php | 558 ++++++++++++++++++++++----
1 file changed, 484 insertions(+), 74 deletions(-)
diff --git a/WebFiori/Framework/Ui/StarterPage.php b/WebFiori/Framework/Ui/StarterPage.php
index 091d95996..9e1521d00 100644
--- a/WebFiori/Framework/Ui/StarterPage.php
+++ b/WebFiori/Framework/Ui/StarterPage.php
@@ -10,120 +10,530 @@
*/
namespace WebFiori\Framework\Ui;
-/**
- * A page which is shown to the framework users when the developer has not
- * configured any routes.
- *
- * @author Ibrahim
- *
- * @version 1.0
- *
- * @since 2.3.0
- */
class StarterPage extends WebPage {
public function __construct() {
parent::__construct();
+
$this->initHead();
$this->initAppScript();
- $this->getChildByID('page-body')->setNodeName('v-app');
+
+ // Vue mount must be a normal div
+ $this->getChildByID('page-body')->setNodeName('div');
+
+ $this->setTitle('Welcome to WebFiori');
+
+ // === ORIGINAL BACKGROUND COLORS ===
$this->getDocument()->getDocumentRoot()->setStyle([
'background-color' => '#e0f2b4'
]);
+
$this->getChildByID(self::MAIN_ELEMENTS[2])->setStyle([
- 'background' => 'rgb(213,238,153)',
- 'background' => 'radial-gradient(circle, rgba(213,238,153,0.5550420851934523) 26%, rgba(4,101,37,0.45700286950717783) 68%)'
+ 'background' => 'radial-gradient(circle, rgba(213,238,153,0.55) 26%, rgba(4,101,37,0.45) 68%)',
+ 'min-height' => '100vh'
]);
- $this->setTitle('Welcome to WebFiori');
- $div = $this->insert('div');
- $div->addChild('img', [
+
+ // Vue root
+ $root = $this->insert('div')->setID('starter-root');
+
+ // CSS: keep palette, reduce inline styles, rely on Vuetify utility classes
+ $style = $this->getDocument()->addChild('style');
+ $style->text("
+:root{
+ --wf-green-900:#1b3a1b;
+ --wf-green-700:#2e4e2e;
+}
+
+/* Keep expansion chevron visible on light surfaces */
+#starter-root .v-expansion-panel-header__icon .v-icon {
+ color: var(--wf-green-900) !important;
+}
+
+/* Page spacing */
+#starter-root .wf-page {
+ padding-bottom: 40px;
+}
+
+/* Hero wrapper */
+#starter-root .wf-hero {
+ background: rgba(255,255,255,.84);
+ border: 1px solid rgba(27,58,27,.14);
+ border-radius: 14px;
+ padding: 22px 18px;
+}
+
+/* Typography helpers */
+#starter-root .wf-title {
+ color: var(--wf-green-900);
+ margin: 14px 0 0;
+ letter-spacing: .2px;
+}
+#starter-root .wf-subtitle {
+ margin-top: 6px;
+ color: var(--wf-green-700);
+ font-size: 14px;
+}
+#starter-root .wf-section-label {
+ color: var(--wf-green-700);
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: .2px;
+ margin: 18px 0 6px;
+}
+
+/* Soft card background */
+#starter-root .wf-soft-card {
+ background: rgba(255,255,255,.82);
+ border: 1px solid rgba(27,58,27,.14);
+}
+
+/* Code blocks */
+#starter-root .wf-code {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, \"Liberation Mono\", \"Courier New\", monospace;
+ font-size: 12.5px;
+ line-height: 1.45;
+ background: rgba(255,255,255,.92);
+ border: 1px solid rgba(27,58,27,.12);
+ border-radius: 8px;
+ padding: 12px;
+ overflow: auto;
+ margin-top: 8px;
+ color: var(--wf-green-900);
+}
+
+/* Clickable cards */
+#starter-root .wf-card-clickable {
+ cursor: pointer;
+ transition: transform .15s ease;
+}
+.transparent{
+background: transparent !important
+}
+#starter-root .wf-card-clickable:hover {
+ transform: translateY(-2px);
+}
+ ", false);
+
+ // ---------- v-app ----------
+ $app = $root->addChild('v-app', [
+ 'class' => 'transparent'
+ ]);
+
+ // Global snackbar for copy feedback
+ $app->addChild('v-snackbar', [
+ 'v-model' => 'snackbar',
+ ':color' => 'snackbarColor',
+ 'timeout' => 2200,
+ 'top',
+ ])->text('{{ snackbarText }}');
+
+ $main = $app->addChild('v-main');
+ $container = $main->addChild('v-container', [
+ 'class' => 'wf-page py-8',
+ ]);
+
+ /**
+ * HERO
+ */
+ $heroRow = $container->addChild('v-row', [
+ 'justify' => 'center'
+ ]);
+
+ $heroCol = $heroRow->addChild('v-col', [
+ 'cols' => 12,
+ 'md' => 8,
+ 'lg' => 7
+ ]);
+
+ $hero = $heroCol->addChild('div', [
+ 'class' => 'wf-hero text-center'
+ ]);
+
+ $hero->addChild('img', [
'src' => 'https://WebFiori.com/assets/images/WFLogo512.png',
- 'style' => 'width:250px;height:250px;border-radius:250px;background-color:black'
+ 'class' => 'mx-auto',
+ 'style' => 'width:200px;height:200px;border-radius:200px;background:black'
]);
- $div->setStyle([
- 'text-align' => 'center'
+
+ $titleWrap = $hero->addChild('div', ['class' => 'mt-2']);
+ $titleWrap->addChild('h2', [
+ 'class' => 'wf-title'
+ ])->text('Welcome to WebFiori');
+
+ $titleWrap->addChild('div', [
+ 'class' => 'wf-subtitle'
+ ])->text('Framework installed and ready. Start by creating a route.');
+
+ $chipRow = $hero->addChild('div', ['class' => 'mt-3']);
+ $chipRow->addChild('v-chip', [
+ 'small',
+ 'color' => 'green lighten-4',
+ 'text-color' => '#1b3a1b',
+ 'class' => 'mx-auto'
+ ])->text('v'.WF_VERSION);
+
+ // App path field (copy icon)
+ $hero->addChild('div', ['class' => 'wf-section-label'])->text('Application path');
+
+ $hero->addChild('v-text-field', [
+ ':value' => 'appPath',
+ 'label' => 'Your application is ready at',
+ 'outlined',
+ 'readonly',
+ 'append-icon' => 'mdi-content-copy',
+ '@click:append' => 'copyText(appPath)',
+ 'hint' => 'Click the copy icon to copy the path',
+ 'persistent-hint',
+ 'class' => 'mt-1'
]);
- $div->addChild('h2')->text('Welcome to WebFiori v'.WF_VERSION);
- $row = $div->addChild('v-container')->addChild('v-row', ['justify' => 'center']);
- $row->addChild('v-col', [
- 'cols' => 12, 'sm' => 12, 'md' => 6
- ])->addChild('v-text-field', [
- 'value' => ROOT_PATH.DS.APP_DIR,
- 'disabled',
- 'label' => 'Your application is ready at'
+ /**
+ * Next steps (collapsed with teaser)
+ */
+ $stepsRow = $container->addChild('v-row', [
+ 'justify' => 'center',
+ 'class' => 'mt-4'
]);
- $cardsRow = $row->addChild('v-col', [
+ $stepsCol = $stepsRow->addChild('v-col', [
'cols' => 12,
- ])->addChild('v-row');
- $this->createCard('https://WebFiori.com/learn',
+ 'md' => 8,
+ 'lg' => 7
+ ]);
+
+ $panels = $stepsCol->addChild('v-expansion-panels', [
+ 'accordion',
+ 'flat'
+ ]);
+
+ $panel = $panels->addChild('v-expansion-panel', [
+ 'class' => 'wf-soft-card'
+ ]);
+
+ $header = $panel->addChild('v-expansion-panel-header', [
+ 'class' => 'py-3'
+ ]);
+
+ $headerWrap = $header->addChild('div');
+
+ $headerTitle = $headerWrap->addChild('div', [
+ 'class' => 'font-weight-bold d-flex align-center'
+ ]);
+ $headerTitle->addChild('v-icon', [
+ 'class' => 'mr-2',
+ 'color' => 'green darken-4'
+ ])->text('mdi-map-marker-path');
+ $headerTitle->addChild('span', [
+ 'style' => 'color:var(--wf-green-900);'
+ ])->text('Next steps (recommended)');
+
+ $headerWrap->addChild('div', [
+ 'class' => 'caption mt-1',
+ 'style' => 'color:var(--wf-green-700);'
+ ])->text('Create a route → point it to a page/controller → test it in the browser.');
+
+ $content = $panel->addChild('v-expansion-panel-content');
+
+ $stepsText = $content->addChild('div', [
+ 'class' => 'pt-2',
+ 'style' => 'color:var(--wf-green-900);'
+ ]);
+
+ $stepsText->addChild('div', [
+ 'class' => 'body-2 mb-3',
+ 'style' => 'color:var(--wf-green-700);'
+ ])->text('A simple, recommended flow to get your first page/API running:');
+
+ $list = $stepsText->addChild('v-list', [
+ 'dense',
+ 'class' => 'transparent pa-0'
+ ]);
+
+ $this->addStep($list, '1', 'Create a route', 'Define the URL contract first (PATH + methods).');
+ $this->addStep($list, '2', 'Point it to a page/controller', 'Use Router::page() for pages or Router::addRoute() for classes/controllers.');
+ $this->addStep($list, '3', 'Test it in the browser', 'Confirm 200/404/405 behavior, then expand functionality.');
+
+ $stepsText->addChild('div', [
+ 'class' => 'font-weight-bold mt-4',
+ 'style' => 'color:var(--wf-green-900);'
+ ])->text('Examples');
+
+ // Example 1
+ $ex1 = $stepsText->addChild('div', [
+ 'class' => 'd-flex align-center justify-space-between mt-3'
+ ]);
+ $ex1->addChild('div', [
+ 'class' => 'body-2',
+ 'style' => 'color:var(--wf-green-700);'
+ ])->text('Home page route (static file):');
+
+ $ex1->addChild('v-btn', [
+ 'small',
+ 'outlined',
+ 'color' => 'green darken-2',
+ '@click' => "copyFromRef('exHome')"
+ ])->text('Copy');
+
+ $stepsText->addChild('pre', [
+ 'class' => 'wf-code',
+ 'ref' => 'exHome'
+ ])->text(
+ "Router::page([\n".
+ " RouteOption::PATH => '/',\n".
+ " RouteOption::TO => 'Home.html'\n".
+ "]);"
+ );
+
+ // Example 2
+ $ex2 = $stepsText->addChild('div', [
+ 'class' => 'd-flex align-center justify-space-between mt-4'
+ ]);
+ $ex2->addChild('div', [
+ 'class' => 'body-2',
+ 'style' => 'color:var(--wf-green-700);'
+ ])->text('Dynamic route parameters (PHP page):');
+
+ $ex2->addChild('v-btn', [
+ 'small',
+ 'outlined',
+ 'color' => 'green darken-2',
+ '@click' => "copyFromRef('exDynamic')"
+ ])->text('Copy');
+
+ $stepsText->addChild('pre', [
+ 'class' => 'wf-code',
+ 'ref' => 'exDynamic'
+ ])->text(
+ "Router::page([\n".
+ " RouteOption::PATH => 'products/{category}/{sub-category}',\n".
+ " RouteOption::TO => 'ViewProductsPage.php'\n".
+ "]);"
+ );
+
+ // Example 3
+ $ex3 = $stepsText->addChild('div', [
+ 'class' => 'd-flex align-center justify-space-between mt-4'
+ ]);
+ $ex3->addChild('div', [
+ 'class' => 'body-2',
+ 'style' => 'color:var(--wf-green-700);'
+ ])->text('API endpoint (controller action):');
+
+ $ex3->addChild('v-btn', [
+ 'small',
+ 'outlined',
+ 'color' => 'green darken-2',
+ '@click' => "copyFromRef('exApi')"
+ ])->text('Copy');
+
+ $stepsText->addChild('pre', [
+ 'class' => 'wf-code',
+ 'ref' => 'exApi'
+ ])->text(
+ "Router::addRoute([\n".
+ " RouteOption::PATH => '/api/add-user',\n".
+ " RouteOption::TO => UsersController::class,\n".
+ " RouteOption::REQUEST_METHODS => ['post', 'put'],\n".
+ " RouteOption::ACTION => 'addUser'\n".
+ "]);"
+ );
+
+ // Divider before cards
+ $container->addChild('v-row', ['justify' => 'center'])
+ ->addChild('v-col', ['cols' => 12, 'md' => 8, 'lg' => 7])
+ ->addChild('v-divider', ['class' => 'my-8']);
+
+ /**
+ * Resource cards
+ */
+ $cardsRow = $container->addChild('v-row', [
+ 'class' => 'mt-2',
+ 'justify' => 'center'
+ ]);
+
+ $this->createCard(
+ 'https://WebFiori.com/learn',
'mdi-book-open-variant',
'Learn',
- 'Documentation is always the first place where developers can find what they need.'
- .'The framework has good documentation base which is still in development and '
- .'content is added and revewed regularly. '
- .'Whether you are new to WebFiori framework or have some '
- .'experience with it, we recommend the '
- .'documentation as they will help in a way or another.',
- $cardsRow->addChild('v-col', ['cols' => 12, 'md' => 6, 'sm' => 12]));
- $this->createCard('https://WebFiori.com/docs/WebFiori',
+ 'Guides, concepts, and examples to get productive fast.',
+ $cardsRow->addChild('v-col', ['cols' => 12, 'md' => 6])
+ );
+
+ $this->createCard(
+ 'https://WebFiori.com/docs/WebFiori',
'mdi-book-check-outline',
'API Reference',
- 'This reference has all information about core framework classes that a developer '
- .'might need to have specific functionality. In addition to that, it describes all '
- .'uses of every public class attribute and method. It can be handy when developers starts '
- .'using advanced features of the framework.',
- $cardsRow->addChild('v-col', ['cols' => 12, 'md' => 6, 'sm' => 12]));
- $this->createCard('https://WebFiori.com/contribute',
+ 'Explore framework classes, attributes, and method usage.',
+ $cardsRow->addChild('v-col', ['cols' => 12, 'md' => 6])
+ );
+
+ $this->createCard(
+ 'https://WebFiori.com/contribute',
'mdi-comment-plus-outline',
'Support The Project',
- 'Want to help in development of the framework or contribute? This place is for you. It holds '
- .'basic instructions on how you may help in supporting the framework in many ways.',
- $cardsRow->addChild('v-col', ['cols' => 12, 'md' => 6, 'sm' => 12]));
+ 'Help improve WebFiori by contributing, reporting issues, or sponsoring.',
+ $cardsRow->addChild('v-col', ['cols' => 12, 'md' => 6])
+ );
+
+ $this->createCard(
+ 'https://github.com/WebFiori/framework/discussions',
+ 'mdi-forum-outline',
+ 'Community Discussions',
+ 'Ask questions, share ideas, and discuss WebFiori with the community.',
+ $cardsRow->addChild('v-col', ['cols' => 12, 'md' => 6])
+ );
+
+ // Footer
+ $container->addChild('div', [
+ 'class' => 'text-center mt-8',
+ 'style' => 'color:var(--wf-green-700);font-size:13px'
+ ])->text('WebFiori v'.WF_VERSION.' • MIT License');
}
+
+ private function addStep($list, $num, $title, $desc) {
+ $item = $list->addChild('v-list-item', [
+ 'class' => 'px-0'
+ ]);
+
+ $item->addChild('v-list-item-icon')
+ ->addChild('v-avatar', [
+ 'size' => 26,
+ 'color' => 'green darken-4'
+ ])->addChild('span', [
+ 'class' => 'white--text',
+ 'style' => 'font-size:12px'
+ ])->text($num);
+
+ $content = $item->addChild('v-list-item-content');
+ $content->addChild('v-list-item-title', [
+ 'class' => 'font-weight-bold',
+ 'style' => 'color:var(--wf-green-900);'
+ ])->text($title);
+
+ $content->addChild('v-list-item-subtitle', [
+ 'class' => 'body-2',
+ 'style' => 'color:var(--wf-green-700);'
+ ])->text($desc);
+ }
+
private function createCard($link, $icon, $cardTitle, $paragraph, \WebFiori\Ui\HTMLNode $el) {
$card = $el->addChild('v-card', [
'hover',
+ 'link',
+ 'href' => $link,
+ 'target' => '_blank',
+ 'rel' => 'noopener',
'height' => '220px',
+ 'class' => 'wf-card-clickable',
'style' => [
- 'background' => 'rgba(255,255,255,.6)'
+ 'background' => 'rgba(255,255,255,.8)'
]
]);
- $card->addChild('v-card-title')->addChild('v-icon',[
- 'style' => 'margin:10px'
- ])
- ->text($icon)
- ->getParent()->addChild('a', [
- 'href' => $link
+
+ $title = $card->addChild('v-card-title', [
+ 'class' => 'pb-1'
+ ]);
+
+ $title->addChild('v-icon', [
+ 'class' => 'mr-2',
+ 'color' => 'green darken-4'
+ ])->text($icon);
+
+ $title->addChild('span', [
+ 'class' => 'font-weight-medium',
+ 'style' => 'color:var(--wf-green-900);'
])->text($cardTitle);
- $card->addChild('v-card-text')->text($paragraph);
+
+ $card->addChild('v-card-text', [
+ 'class' => 'pt-2'
+ ])->text($paragraph);
}
+
private function initAppScript() {
- $script = $this->getDocument()->addChild('script');
- $script->text(""
- ."new Vue({"
- ." el:'#page-body',"
- ." vuetify:new Vuetify({"
- ." theme: {"
- ." dark:false,"
- ." themes:{"
- ." dark:{},"
- ." light:{}"
- ." }"
- ." }"
- ." })"
- ."});"
- .""
- .""
- .""
- ."", false);
+ $appPath = json_encode(ROOT_PATH.DS.APP_DIR, JSON_UNESCAPED_SLASHES);
+
+ $this->getDocument()->addChild('script')->text("
+new Vue({
+ el: '#starter-root',
+ vuetify: new Vuetify(),
+ data: function () {
+ return {
+ appPath: {$appPath},
+ snackbar: false,
+ snackbarText: '',
+ snackbarColor: 'success'
+ };
+ },
+ methods: {
+ copyFromRef: function (refName) {
+ var el = this.\$refs[refName];
+ if (!el) {
+ this.snackbarColor = 'error';
+ this.snackbarText = 'Nothing to copy.';
+ this.snackbar = true;
+ return;
+ }
+ this.copyText(el.innerText);
+ },
+
+ copyText: function (text) {
+ var self = this;
+ if (!text) {
+ self.snackbarColor = 'error';
+ self.snackbarText = 'Nothing to copy.';
+ self.snackbar = true;
+ return;
+ }
+
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(function () {
+ self.snackbarColor = 'success';
+ self.snackbarText = 'Copied!';
+ self.snackbar = true;
+ }).catch(function () {
+ self.fallbackCopy(text);
+ });
+ return;
+ }
+
+ self.fallbackCopy(text);
+ },
+
+ fallbackCopy: function (text) {
+ try {
+ var ta = document.createElement('textarea');
+ ta.value = text;
+ ta.setAttribute('readonly', '');
+ ta.style.position = 'fixed';
+ ta.style.top = '-9999px';
+ document.body.appendChild(ta);
+ ta.select();
+ document.execCommand('copy');
+ document.body.removeChild(ta);
+
+ this.snackbarColor = 'success';
+ this.snackbarText = 'Copied!';
+ this.snackbar = true;
+ } catch (e) {
+ this.snackbarColor = 'error';
+ this.snackbarText = 'Copy failed. Please copy manually.';
+ this.snackbar = true;
+ }
}
+ }
+});
+ ", false);
+ }
+
private function initHead() {
$head = $this->getDocument()->getHeadNode();
- $head->addJs('https://unpkg.com/vue@2.x.x');
+
+ $head->addMeta('viewport', 'width=device-width, initial-scale=1');
+
$head->addCSS('https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900');
$head->addCSS('https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css');
$head->addCSS('https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css');
+
+ $head->addJs('https://unpkg.com/vue@2.x.x');
$head->addJs('https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js');
}
}
From 6e74dd27c84d620684a82d6b58977d05eccacbda Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 4 Feb 2026 00:39:00 +0300
Subject: [PATCH 52/88] Update RestServiceWriter.php
---
.../Framework/Writers/RestServiceWriter.php | 56 ++++++++++---------
1 file changed, 30 insertions(+), 26 deletions(-)
diff --git a/WebFiori/Framework/Writers/RestServiceWriter.php b/WebFiori/Framework/Writers/RestServiceWriter.php
index 0ff9b862e..0f7edf50f 100644
--- a/WebFiori/Framework/Writers/RestServiceWriter.php
+++ b/WebFiori/Framework/Writers/RestServiceWriter.php
@@ -1,4 +1,5 @@
setSuffix('Service');
@@ -29,17 +30,13 @@ public function __construct() {
'WebFiori\\Http\\Annotations\\PostMapping',
'WebFiori\\Http\\Annotations\\PutMapping',
'WebFiori\\Http\\Annotations\\DeleteMapping',
- 'WebFiori\\Http\\Annotations\\Param',
+ 'WebFiori\\Http\\Annotations\\RequestParam',
'WebFiori\\Http\\Annotations\\ResponseBody',
'WebFiori\\Http\\Annotations\\AllowAnonymous',
'WebFiori\\Http\\ParamType'
]);
}
-
- public function setDescription(string $desc) {
- $this->description = $desc;
- }
-
+
public function addMethod(string $httpMethod, string $methodName, array $params = [], string $returnType = 'array') {
$this->methods[] = [
'http' => $httpMethod,
@@ -48,14 +45,18 @@ public function addMethod(string $httpMethod, string $methodName, array $params
'return' => $returnType
];
}
-
+
+ public function setDescription(string $desc) {
+ $this->description = $desc;
+ }
+
public function writeClassBody() {
foreach ($this->methods as $method) {
$this->writeMethod($method);
}
$this->append('}');
}
-
+
public function writeClassComment() {
$serviceName = strtolower(str_replace('Service', '', $this->getName()));
$this->append('/**');
@@ -63,53 +64,56 @@ public function writeClassComment() {
$this->append(' */');
$this->append("#[RestController('$serviceName', '{$this->description}')]", 0);
}
-
+
public function writeClassDeclaration() {
$this->append('class '.$this->getName().' extends WebService {');
}
-
+
+ private function mapParamType(string $type): string {
+ return match ($type) {
+ 'INT' => 'int',
+ 'STRING', 'EMAIL', 'URL' => 'string',
+ 'DOUBLE' => 'float',
+ 'BOOL' => 'bool',
+ 'ARRAY' => 'array',
+ default => 'string'
+ };
+ }
+
private function writeMethod(array $method) {
$this->append('', 1);
$mapping = ucfirst(strtolower($method['http'])).'Mapping';
$this->append("#[$mapping]", 1);
$this->append('#[ResponseBody]', 1);
$this->append('#[AllowAnonymous]', 1);
-
+
foreach ($method['params'] as $param) {
- $paramAttr = "#[Param('{$param['name']}', ParamType::{$param['type']}, '{$param['description']}'";
+ $paramAttr = "#[RequestParam('{$param['name']}', ParamType::{$param['type']}, '{$param['description']}'";
+
if (isset($param['min'])) {
$paramAttr .= ", min: {$param['min']}";
}
+
if (isset($param['max'])) {
$paramAttr .= ", max: {$param['max']}";
}
$paramAttr .= ')]';
$this->append($paramAttr, 1);
}
-
+
$signature = 'public function '.$method['name'].'(';
$paramList = [];
+
foreach ($method['params'] as $param) {
$type = $this->mapParamType($param['type']);
$paramList[] = "?$type \${$param['name']} = null";
}
$signature .= implode(', ', $paramList);
$signature .= '): '.$method['return'].' {';
-
+
$this->append($signature, 1);
$this->append('// TODO: Implement method logic', 2);
$this->append('return [];', 2);
$this->append('}', 1);
}
-
- private function mapParamType(string $type): string {
- return match($type) {
- 'INT' => 'int',
- 'STRING', 'EMAIL', 'URL' => 'string',
- 'DOUBLE' => 'float',
- 'BOOL' => 'bool',
- 'ARRAY' => 'array',
- default => 'string'
- };
- }
}
From 3e64be3523a86ddfc4bd87549963f826d31cdfd6 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 4 Feb 2026 18:39:00 +0300
Subject: [PATCH 53/88] chore: Remove Non-Needed Files
---
App/Init/InitAutoLoad.php | 13 -------------
App/Init/InitCliCommands.php | 12 ------------
App/Init/InitCommands.php | 13 -------------
App/Init/InitMiddleware.php | 13 -------------
App/Init/InitPrivileges.php | 13 -------------
App/Init/InitTasks.php | 13 -------------
App/Init/routes/APIsRoutes.php | 12 ------------
App/Init/routes/ClosureRoutes.php | 12 ------------
App/Init/routes/OtherRoutes.php | 12 ------------
App/Init/routes/PagesRoutes.php | 12 ------------
10 files changed, 125 deletions(-)
delete mode 100644 App/Init/InitAutoLoad.php
delete mode 100644 App/Init/InitCliCommands.php
delete mode 100644 App/Init/InitCommands.php
delete mode 100644 App/Init/InitMiddleware.php
delete mode 100644 App/Init/InitPrivileges.php
delete mode 100644 App/Init/InitTasks.php
delete mode 100644 App/Init/routes/APIsRoutes.php
delete mode 100644 App/Init/routes/ClosureRoutes.php
delete mode 100644 App/Init/routes/OtherRoutes.php
delete mode 100644 App/Init/routes/PagesRoutes.php
diff --git a/App/Init/InitAutoLoad.php b/App/Init/InitAutoLoad.php
deleted file mode 100644
index 463d2d4c3..000000000
--- a/App/Init/InitAutoLoad.php
+++ /dev/null
@@ -1,13 +0,0 @@
-
Date: Wed, 4 Feb 2026 18:40:28 +0300
Subject: [PATCH 54/88] chore: CS Fixer
---
WebFiori/Framework/App.php | 362 +++++++++++++++++++------------------
1 file changed, 191 insertions(+), 171 deletions(-)
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 005ae1e2b..a49ed5a71 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -1,4 +1,5 @@
initThemesPath();
-
- if (!class_exists(APP_DIR.'\\Init\\InitPrivileges')) {
- Ini::get()->createIniClass('InitPrivileges', 'Initialize user groups and privileges.');
+
+ if (!class_exists(APP_DIR.'\\Ini\\Privileges')) {
+ Ini::get()->createIniClass('Privileges', 'Initialize user groups and privileges.');
}
//Initialize privileges.
//This step must be done before initializing anything.
- self::call(APP_DIR.'\\Init\\InitPrivileges::init');
+ self::call(APP_DIR.'\\Ini\\Privileges::initialize');
$this->initMiddleware();
$this->initRoutes();
@@ -158,6 +160,7 @@ private function __construct() {
register_shutdown_function(function()
{
$uriObj = Router::getRouteUri();
+
if ($uriObj !== null) {
$mdArr = $uriObj->getMiddleware();
@@ -288,56 +291,167 @@ public static function getConfigDriver() : string {
return self::$ConfigDriver;
}
/**
- * Calculates application root path by removing vendor framework path from current directory.
- *
- * @return string The application root path.
+ * Returns the current request instance.
+ *
+ * @return Request
*/
- private static function getRoot() {
- //Following lines of code assumes that the class exist on the folder:
- //\vendor\WebFiori\framework\WebFiori\Framework
- //Its used to construct the folder at which index file will exist at
- $DS = DIRECTORY_SEPARATOR;
- $vendorPath = $DS.'vendor'.$DS.'webFiori'.$DS.'framework'.$DS.'WebFiori'.$DS.'Framework';
- $rootPath = substr(__DIR__, 0, strlen(__DIR__) - strlen($vendorPath));
- return $rootPath;
+ public static function getRequest() : Request {
+ return self::$Request;
+ }
+ /**
+ * Returns the current response instance.
+ *
+ * @return Response
+ */
+ public static function getResponse() : Response {
+ return self::$Response;
+ }
+ /**
+ * Returns an instance which represents the class that is used to run the
+ * terminal.
+ *
+ * @return Runner
+ * @throws FileException
+ */
+ public static function getRunner() : Runner {
+ if (!class_exists(APP_DIR.'\Ini\InitCommands')) {
+ Ini::get()->createIniClass('InitCommands', 'A method that can be used to initialize CLI commands.');
+ }
+
+ if (self::$CliRunner === null) {
+ self::$CliRunner = new Runner();
+
+ if (Runner::isCLI()) {
+ if (defined('CLI_HTTP_HOST')) {
+ $host = CLI_HTTP_HOST;
+ } else {
+ $host = '127.0.0.1';
+ define('CLI_HTTP_HOST', $host);
+ }
+ $_SERVER['HTTP_HOST'] = $host;
+ $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
+
+ if (defined('ROOT_PATH')) {
+ $_SERVER['DOCUMENT_ROOT'] = ROOT_PATH;
+ }
+ $_SERVER['REQUEST_URI'] = '/';
+ putenv('HTTP_HOST='.$host);
+ putenv('REQUEST_URI=/');
+
+ if (defined('USE_HTTP') && USE_HTTP === true) {
+ $_SERVER['HTTPS'] = 'no';
+ } else {
+ $_SERVER['HTTPS'] = 'yes';
+ }
+ }
+ self::$CliRunner->setBeforeStart(function (Runner $r)
+ {
+ $commands = [
+ '\\WebFiori\\Framework\\Cli\\Commands\\WHelpCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\VersionCommand',
+
+ '\\WebFiori\\Framework\\Cli\\Commands\\SchedulerCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\AddDbConnectionCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\AddSmtpConnectionCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\AddLangCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateMiddlewareCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateTaskCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateCommandCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateEntityCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateServiceCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateTableCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateRepositoryCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateResourceCommand',
+
+
+
+
+
+ '\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommand',
+ ];
+
+ foreach ($commands as $c) {
+ $r->register(new $c());
+ }
+ $r->setDefaultCommand('help');
+ self::call(APP_DIR.'\Init\InitCommands::init');
+ });
+ }
+
+ return self::$CliRunner;
}
/**
* Handel the request.
- *
+ *
* This method should only be called after the application has been initialized.
* Its used to handle HTTP requests or start CLI processing.
*/
public static function handle() {
-
if (self::$ClassStatus == self::STATUS_NONE) {
$publicFolderName = 'public';
self::initiate('App', $publicFolderName, self::getRoot().DIRECTORY_SEPARATOR.$publicFolderName);
}
+
if (self::$ClassStatus == self::STATUS_INITIATED) {
self::start();
}
+
if (self::$ClassStatus == self::STATUS_INITIALIZED) {
if (App::getRunner()->isCLI() === true) {
App::getRunner()->start();
} else {
- //route user request.
- Router::route(self::getRequest()->getRequestedURI());
- self::getResponse()->send();
+ //route user request.
+ Router::route(self::getRequest()->getRequestedURI());
+ self::getResponse()->send();
}
}
}
+ /**
+ * Initialize global constants which has information about framework version.
+ *
+ * The constants which are defined by this method include the following:
+ *
+ * - WF_VERSION: A string such as '3.0.0'.
+ * - WF_VERSION_TYPE: Type of the release such as 'RC', 'Alpha' or 'Stable'.
+ * - WF_RELEASE_DATE: The date at which the specified version was created at.
+ *
+ */
+ public static function initFrameworkVersionInfo() {
+ /**
+ * A constant that represents version number of the framework.
+ *
+ * @since 2.1
+ */
+ define('WF_VERSION', '3.0.0-beta.31');
+ /**
+ * A constant that tells the type of framework version.
+ *
+ * The constant can have values such as 'Alpha', 'Beta' or 'Stable'.
+ *
+ * @since 2.1
+ */
+ define('WF_VERSION_TYPE', 'Beta');
+ /**
+ * The date at which the framework version was released.
+ *
+ * The value of the constant will be a string in the format YYYY-MM-DD.
+ *
+ * @since 2.1
+ */
+ define('WF_RELEASE_DATE', '2025-10-28');
+ }
/**
* Initiate main components of the application.
- *
+ *
* This method is intended to be called in the index file of the project.
* It should be first thing to be called.
- *
+ *
* @param string $appFolder The name of the folder at which the application
* is created at.
- *
+ *
* @param string $publicFolder A string that represent the name of the public
* folder such as 'public'.
- *
+ *
* @param string $indexDir The directory at which index file exist at.
* Usually, its the value of the constant __DIR__.
*/
@@ -351,12 +465,14 @@ public static function initiate(string $appFolder = 'App', string $publicFolder
mb_http_output($encoding);
mb_regex_encoding($encoding);
}
+
if (!defined('DS')) {
/**
* Directory separator.
*/
define('DS', DIRECTORY_SEPARATOR);
}
+
if (!defined('ROOT_PATH')) {
if ($indexDir == __DIR__) {
$indexDir = self::getRoot().DS.$publicFolder;
@@ -366,25 +482,28 @@ public static function initiate(string $appFolder = 'App', string $publicFolder
*/
define('ROOT_PATH', substr($indexDir,0, strlen($indexDir) - strlen(DS.$publicFolder)));
}
+
if (!defined('APP_DIR')) {
/**
* Name of application directory.
*/
define('APP_DIR', $appFolder);
}
+
if (!defined('APP_PATH')) {
/**
* Path to application directory.
*/
define('APP_PATH', ROOT_PATH.DIRECTORY_SEPARATOR.APP_DIR.DS);
}
+
if (!defined('PUBLIC_FOLDER')) {
/**
* Name of public folder.
*/
define('PUBLIC_FOLDER', $publicFolder);
}
-
+
if (!defined('WF_CORE_PATHS')) {
/**
* Possible Paths to WebFiori's core library.
@@ -400,80 +519,6 @@ public static function initiate(string $appFolder = 'App', string $publicFolder
self::initFrameworkVersionInfo();
self::$ClassStatus = self::STATUS_INITIATED;
}
- /**
- * Returns an instance which represents the class that is used to run the
- * terminal.
- *
- * @return Runner
- * @throws FileException
- */
- public static function getRunner() : Runner {
- if (!class_exists(APP_DIR.'\Init\InitCommands')) {
- Ini::get()->createIniClass('InitCommands', 'A method that can be used to initialize CLI commands.');
- }
-
- if (self::$CliRunner === null) {
- self::$CliRunner = new Runner();
-
- if (Runner::isCLI()) {
- if (defined('CLI_HTTP_HOST')) {
- $host = CLI_HTTP_HOST;
- } else {
- $host = '127.0.0.1';
- define('CLI_HTTP_HOST', $host);
- }
- $_SERVER['HTTP_HOST'] = $host;
- $_SERVER['REMOTE_ADDR'] = '127.0.0.1';
-
- if (defined('ROOT_PATH')) {
- $_SERVER['DOCUMENT_ROOT'] = ROOT_PATH;
- }
- $_SERVER['REQUEST_URI'] = '/';
- putenv('HTTP_HOST='.$host);
- putenv('REQUEST_URI=/');
-
- if (defined('USE_HTTP') && USE_HTTP === true) {
- $_SERVER['HTTPS'] = 'no';
- } else {
- $_SERVER['HTTPS'] = 'yes';
- }
- }
- self::$CliRunner->setBeforeStart(function (Runner $r)
- {
- $commands = [
- '\\WebFiori\\Framework\\Cli\\Commands\\WHelpCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\VersionCommand',
-
- '\\WebFiori\\Framework\\Cli\\Commands\\SchedulerCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\AddDbConnectionCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\AddSmtpConnectionCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\AddLangCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateMiddlewareCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateTaskCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateCommandCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateEntityCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateServiceCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateTableCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateRepositoryCommand',
- '\\WebFiori\\Framework\\Cli\\Commands\\CreateResourceCommand',
-
-
-
-
-
- '\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommand',
- ];
-
- foreach ($commands as $c) {
- $r->register(new $c());
- }
- $r->setDefaultCommand('help');
- self::call(APP_DIR.'\Init\InitCommands::init');
- });
- }
-
- return self::$CliRunner;
- }
/**
* Sets the class that will be used as configuration driver.
*
@@ -508,7 +553,7 @@ public static function start(): App {
}
/**
* Helper for automatic class registration using reflection with configuration options.
- *
+ *
* @param array $options Configuration array with dir, php-file, folder, class-name, params, callback, constructor-params.
*/
private static function autoRegisterHelper($options) {
@@ -539,12 +584,14 @@ private static function autoRegisterHelper($options) {
}
/**
* Safe function caller with CLI/web-aware exception handling.
- *
+ *
* @param callable $func The function to call.
+ *
+ * @return mixed
*/
private static function call($func) {
try {
- call_user_func($func);
+ return call_user_func($func);
} catch (Exception $ex) {
if (self::getRunner()->isCLI()) {
printf("WARNING: ".$ex->getMessage().' at '.$ex->getFile().':'.$ex->getLine()."\n");
@@ -557,7 +604,6 @@ private static function call($func) {
* Validates and defines APP_DIR constant, checking for invalid characters.
*/
private function checkAppDir() {
-
if (!defined('APP_DIR')) {
/**
* The name of the directory at which the developer will have his own application
@@ -652,6 +698,21 @@ private static function checkStdInOut() {
define('STDERR',fopen('php://stderr', 'w'));
}
}
+ /**
+ * Calculates application root path by removing vendor framework path from current directory.
+ *
+ * @return string The application root path.
+ */
+ private static function getRoot() {
+ //Following lines of code assumes that the class exist on the folder:
+ //\vendor\WebFiori\framework\WebFiori\Framework
+ //Its used to construct the folder at which index file will exist at
+ $DS = DIRECTORY_SEPARATOR;
+ $vendorPath = $DS.'vendor'.$DS.'webFiori'.$DS.'framework'.$DS.'WebFiori'.$DS.'Framework';
+ $rootPath = substr(__DIR__, 0, strlen(__DIR__) - strlen($vendorPath));
+
+ return $rootPath;
+ }
/**
* @throws FileException
@@ -674,50 +735,18 @@ private static function initAutoLoader() {
self::$AU = ClassLoader::get();
$isLoaded = true;
}
- if (!class_exists(APP_DIR.'\\Init\\InitAutoLoad')) {
+
+ if (!class_exists(APP_DIR.'\\Ini\\AutoLoad')) {
Ini::createAppDirs();
- Ini::get()->createIniClass('InitAutoLoad', 'Add user-defined directories to the set of directories at which the framework will search for classes.');
+ Ini::get()->createIniClass('AutoLoad', 'Add user-defined directories to the set of directories at which the framework will search for classes.');
}
- self::call(APP_DIR.'\\Init\\InitAutoLoad::init');
+ self::call(APP_DIR.'\\Ini\\AutoLoad::initialize');
}
+
if (!$isLoaded) {
- throw new \Exception('Unable to locate the autoloader class.');
+ throw new Exception('Unable to locate the autoloader class.');
}
}
- /**
- * Initialize global constants which has information about framework version.
- *
- * The constants which are defined by this method include the following:
- *
- * - WF_VERSION: A string such as '3.0.0'.
- * - WF_VERSION_TYPE: Type of the release such as 'RC', 'Alpha' or 'Stable'.
- * - WF_RELEASE_DATE: The date at which the specified version was created at.
- *
- */
- public static function initFrameworkVersionInfo() {
- /**
- * A constant that represents version number of the framework.
- *
- * @since 2.1
- */
- define('WF_VERSION', '3.0.0-beta.31');
- /**
- * A constant that tells the type of framework version.
- *
- * The constant can have values such as 'Alpha', 'Beta' or 'Stable'.
- *
- * @since 2.1
- */
- define('WF_VERSION_TYPE', 'Beta');
- /**
- * The date at which the framework version was released.
- *
- * The value of the constant will be a string in the format YYYY-MM-DD.
- *
- * @since 2.1
- */
- define('WF_RELEASE_DATE', '2025-10-28');
- }
/**
* @throws FileException
@@ -728,11 +757,11 @@ private function initMiddleware() {
MiddlewareManager::register($inst);
});
- if (!class_exists(APP_DIR.'\Init\InitMiddleware')) {
- Ini::get()->createIniClass('InitMiddleware', 'Register middleware which are created outside the folder \'[APP_DIR]/Middleware\'.');
+ if (!class_exists(APP_DIR.'\Ini\Middleware')) {
+ Ini::get()->createIniClass('Middleware', 'Register middleware which are created outside the folder \'[APP_DIR]/Middleware\'.');
}
MiddlewareManager::register(new StartSessionMiddleware());
- self::call(APP_DIR.'\Init\InitMiddleware::init');
+ self::call(APP_DIR.'\Ini\Middleware::initialize');
}
/**
* @throws FileException
@@ -741,12 +770,18 @@ private function initRoutes() {
$routesClasses = ['APIsRoutes', 'PagesRoutes', 'ClosureRoutes', 'OtherRoutes'];
foreach ($routesClasses as $className) {
- if (!class_exists(APP_DIR.'\\Init\\Routes\\'.$className)) {
+ if (!class_exists(APP_DIR.'\\Ini\\Routes\\'.$className)) {
Ini::get()->createRoutesClass($className);
}
- self::call(APP_DIR.'\Init\Routes\\'.$className.'::create');
- }
+ $routesArr = self::call(APP_DIR.'\Ini\Routes\\'.$className.'::create');
+ if (gettype($routesArr) == 'array') {
+ foreach ($routesArr as $route) {
+ Router::addRoute($route);
+ }
+ }
+ }
+
if (Router::routesCount() != 0) {
$home = trim(self::getConfig()->getHomePage());
@@ -763,8 +798,8 @@ private function initScheduler() {
$uriObj = new RouterUri(self::getRequest()->getUri()->getUri(true, true), '');
$pathArr = $uriObj->getPathArray();
- if (!class_exists(APP_DIR.'\Init\InitTasks')) {
- Ini::get()->createIniClass('InitTasks', 'A method that can be used to register background tasks.');
+ if (!class_exists(APP_DIR.'\Ini\Tasks')) {
+ Ini::get()->createIniClass('Tasks', 'A method that can be used to register background tasks.');
}
if (Runner::isCLI() || (defined('SCHEDULER_THROUGH_HTTP') && SCHEDULER_THROUGH_HTTP && in_array('scheduler', $pathArr))) {
@@ -773,7 +808,7 @@ private function initScheduler() {
}
TasksManager::getPassword(self::getConfig()->getSchedulerPassword());
//initialize scheduler tasks only if in CLI or scheduler is enabled through HTTP.
- self::call(APP_DIR.'\Init\InitTasks::init');
+ self::call(APP_DIR.'\Ini\Tasks::initialize');
TasksManager::registerTasks();
}
}
@@ -799,21 +834,6 @@ private function setHandlers() {
// Handler::registerHandler(new APICallErrHandler());
// Handler::registerHandler(new HTTPErrHandler());
// Handler::unregisterHandler(Handler::getHandler('Default'));
- }
- /**
- * Returns the current request instance.
- *
- * @return Request
- */
- public static function getRequest() : Request {
- return self::$Request;
- }
- /**
- * Returns the current response instance.
- *
- * @return Response
- */
- public static function getResponse() : Response {
- return self::$Response;
+ Handler::setConfig(HandlerConfig::createDevelopmentConfig());
}
}
From 3198d56d1b59b4d2af6bd559298d364427c8913c Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Sun, 8 Feb 2026 23:01:47 +0300
Subject: [PATCH 55/88] refactor: Rename Initialization Class
---
WebFiori/Framework/App.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index a49ed5a71..9367e843f 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -314,8 +314,8 @@ public static function getResponse() : Response {
* @throws FileException
*/
public static function getRunner() : Runner {
- if (!class_exists(APP_DIR.'\Ini\InitCommands')) {
- Ini::get()->createIniClass('InitCommands', 'A method that can be used to initialize CLI commands.');
+ if (!class_exists(APP_DIR.'\Ini\Commands')) {
+ Ini::get()->createIniClass('Commands', 'A method that can be used to register custom CLI commands.');
}
if (self::$CliRunner === null) {
@@ -374,7 +374,7 @@ public static function getRunner() : Runner {
$r->register(new $c());
}
$r->setDefaultCommand('help');
- self::call(APP_DIR.'\Init\InitCommands::init');
+ self::call(APP_DIR.'\Ini\Commands::init');
});
}
@@ -781,7 +781,7 @@ private function initRoutes() {
}
}
}
-
+
if (Router::routesCount() != 0) {
$home = trim(self::getConfig()->getHomePage());
From 94775c56e8cc5394767fd530fd7563a481960e02 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Sun, 8 Feb 2026 23:14:05 +0300
Subject: [PATCH 56/88] chore: Run CS Fixer
---
.../migrations/multi/Migration000.php | 16 +-
.../migrations/multi/Migration001.php | 16 +-
.../migrations/multi/Migration002.php | 17 +-
App/Database/migrations/multi/MultiRunner.php | 4 -
.../migrations/multiDownErr/Migration000.php | 14 +-
.../migrations/multiDownErr/Migration001.php | 16 +-
.../migrations/multiDownErr/Migration002.php | 16 +-
.../multiDownErr/MultiErrRunner.php | 3 -
.../migrations/multiErr/Migration000.php | 16 +-
.../migrations/multiErr/Migration001.php | 16 +-
.../migrations/multiErr/Migration002.php | 16 +-
.../migrations/multiErr/MultiErrRunner.php | 1 -
.../migrations/noConn/Migration000.php | 16 +-
App/Database/migrations/noConn/XRunner.php | 1 -
php_cs.php.dist | 2 +-
public/index.php | 1 -
release-commit.php | 2 +-
tests/Apis/Multiple/WebService00.php | 10 +-
tests/Apis/Multiple/WebService01.php | 3 +-
.../Tests/Cli/AddDbConnectionCommandTest.php | 8 +-
.../Tests/Cli/AddLangCommandTest.php | 15 +-
.../Cli/AddSmtpConnectionCommandTest.php | 2 +-
.../Tests/Cli/CreateCommandCommandTest.php | 18 +-
.../Tests/Cli/CreateEntityCommandTest.php | 18 +-
.../Tests/Cli/CreateMiddlewareCommandTest.php | 28 +-
.../Tests/Cli/CreateRepositoryCommandTest.php | 16 +-
.../Tests/Cli/CreateServiceCommandTest.php | 16 +-
.../Tests/Cli/CreateTableCommandTest.php | 16 +-
.../Tests/Cli/CreateTaskCommandTest.php | 22 +-
.../Framework/Tests/Cli/HelpCommandTest.php | 2 +-
.../Tests/Cli/RunMigrationsCommandTest.php | 139 ++--
.../Tests/Cli/SchedulerCommandTest.php | 399 ++++++-----
.../Tests/Cli/VersionCommandTest.php | 2 +-
.../Framework/Tests/Config/JsonDriverTest.php | 661 +++++++++---------
34 files changed, 766 insertions(+), 782 deletions(-)
diff --git a/App/Database/migrations/multi/Migration000.php b/App/Database/migrations/multi/Migration000.php
index f3f9c4cb7..72155beef 100644
--- a/App/Database/migrations/multi/Migration000.php
+++ b/App/Database/migrations/multi/Migration000.php
@@ -14,19 +14,19 @@ public function __construct() {
parent::__construct('Third One', 2);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) : void {
- //TODO: Implement the action which will apply the migration to database.
+ public function down(Database $schema) : void {
+ //TODO: Implement the action which will revert back the migration.
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) : void {
- //TODO: Implement the action which will revert back the migration.
+ public function up(Database $schema) : void {
+ //TODO: Implement the action which will apply the migration to database.
}
}
diff --git a/App/Database/migrations/multi/Migration001.php b/App/Database/migrations/multi/Migration001.php
index 12e3b8759..83fc19f21 100644
--- a/App/Database/migrations/multi/Migration001.php
+++ b/App/Database/migrations/multi/Migration001.php
@@ -14,19 +14,19 @@ public function __construct() {
parent::__construct('Second one', 1);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) : void {
- //TODO: Implement the action which will apply the migration to database.
+ public function down(Database $schema) : void {
+ //TODO: Implement the action which will revert back the migration.
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) : void{
- //TODO: Implement the action which will revert back the migration.
+ public function up(Database $schema) : void {
+ //TODO: Implement the action which will apply the migration to database.
}
}
diff --git a/App/Database/migrations/multi/Migration002.php b/App/Database/migrations/multi/Migration002.php
index f8aa4de91..957777bf1 100644
--- a/App/Database/migrations/multi/Migration002.php
+++ b/App/Database/migrations/multi/Migration002.php
@@ -14,20 +14,19 @@ public function __construct() {
parent::__construct('First One', 0);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) : void {
- //TODO: Implement the action which will apply the migration to database.
+ public function down(Database $schema) : void {
+ //TODO: Implement the action which will revert back the migration.
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) : void {
- //TODO: Implement the action which will revert back the migration.
+ public function up(Database $schema) : void {
+ //TODO: Implement the action which will apply the migration to database.
}
-
}
diff --git a/App/Database/migrations/multi/MultiRunner.php b/App/Database/migrations/multi/MultiRunner.php
index f59e898aa..ac5172213 100644
--- a/App/Database/migrations/multi/MultiRunner.php
+++ b/App/Database/migrations/multi/MultiRunner.php
@@ -1,15 +1,11 @@
'true'
diff --git a/App/Database/migrations/multiDownErr/Migration000.php b/App/Database/migrations/multiDownErr/Migration000.php
index 88d232e46..6c905b73a 100644
--- a/App/Database/migrations/multiDownErr/Migration000.php
+++ b/App/Database/migrations/multiDownErr/Migration000.php
@@ -14,19 +14,17 @@ public function __construct() {
parent::__construct('Third One', 2);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) {
-
+ public function down(Database $schema) {
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) {
-
+ public function up(Database $schema) {
}
}
diff --git a/App/Database/migrations/multiDownErr/Migration001.php b/App/Database/migrations/multiDownErr/Migration001.php
index d0bacdc3f..937c4a383 100644
--- a/App/Database/migrations/multiDownErr/Migration001.php
+++ b/App/Database/migrations/multiDownErr/Migration001.php
@@ -14,19 +14,19 @@ public function __construct() {
parent::__construct('Second one', 1);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) {
- //TODO: Implement the action which will apply the migration to database.
+ public function down(Database $schema) {
+ $schema->do();
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) {
- $schema->do();
+ public function up(Database $schema) {
+ //TODO: Implement the action which will apply the migration to database.
}
}
diff --git a/App/Database/migrations/multiDownErr/Migration002.php b/App/Database/migrations/multiDownErr/Migration002.php
index a507d52a5..94bcb98c1 100644
--- a/App/Database/migrations/multiDownErr/Migration002.php
+++ b/App/Database/migrations/multiDownErr/Migration002.php
@@ -14,19 +14,19 @@ public function __construct() {
parent::__construct('First One', 0);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) {
- //TODO: Implement the action which will apply the migration to database.
+ public function down(Database $schema) {
+ //TODO: Implement the action which will revert back the migration.
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) {
- //TODO: Implement the action which will revert back the migration.
+ public function up(Database $schema) {
+ //TODO: Implement the action which will apply the migration to database.
}
}
diff --git a/App/Database/migrations/multiDownErr/MultiErrRunner.php b/App/Database/migrations/multiDownErr/MultiErrRunner.php
index ae0d847ab..99094ad45 100644
--- a/App/Database/migrations/multiDownErr/MultiErrRunner.php
+++ b/App/Database/migrations/multiDownErr/MultiErrRunner.php
@@ -3,12 +3,9 @@
use WebFiori\Database\ConnectionInfo;
use WebFiori\Database\Schema\SchemaRunner;
-use const APP_PATH;
-use const DS;
class MultiErrRunner extends SchemaRunner {
-
public function __construct() {
$conn = new ConnectionInfo('mssql', SQL_SERVER_USER, SQL_SERVER_PASS, SQL_SERVER_DB, SQL_SERVER_HOST, 1433, [
'TrustServerCertificate' => 'true'
diff --git a/App/Database/migrations/multiErr/Migration000.php b/App/Database/migrations/multiErr/Migration000.php
index bff12de45..6e024ac4e 100644
--- a/App/Database/migrations/multiErr/Migration000.php
+++ b/App/Database/migrations/multiErr/Migration000.php
@@ -14,19 +14,19 @@ public function __construct() {
parent::__construct('Third One', 2);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) {
- $this->x();
+ public function down(Database $schema) {
+ $schema->y();
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) {
- $schema->y();
+ public function up(Database $schema) {
+ $this->x();
}
}
diff --git a/App/Database/migrations/multiErr/Migration001.php b/App/Database/migrations/multiErr/Migration001.php
index b3efc20ba..2ca424b04 100644
--- a/App/Database/migrations/multiErr/Migration001.php
+++ b/App/Database/migrations/multiErr/Migration001.php
@@ -14,19 +14,19 @@ public function __construct() {
parent::__construct('Second one', 1);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) {
- //TODO: Implement the action which will apply the migration to database.
+ public function down(Database $schema) {
+ //TODO: Implement the action which will revert back the migration.
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) {
- //TODO: Implement the action which will revert back the migration.
+ public function up(Database $schema) {
+ //TODO: Implement the action which will apply the migration to database.
}
}
diff --git a/App/Database/migrations/multiErr/Migration002.php b/App/Database/migrations/multiErr/Migration002.php
index 91a834270..809b3f145 100644
--- a/App/Database/migrations/multiErr/Migration002.php
+++ b/App/Database/migrations/multiErr/Migration002.php
@@ -14,19 +14,19 @@ public function __construct() {
parent::__construct('First One', 0);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) {
- //TODO: Implement the action which will apply the migration to database.
+ public function down(Database $schema) {
+ //TODO: Implement the action which will revert back the migration.
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) {
- //TODO: Implement the action which will revert back the migration.
+ public function up(Database $schema) {
+ //TODO: Implement the action which will apply the migration to database.
}
}
diff --git a/App/Database/migrations/multiErr/MultiErrRunner.php b/App/Database/migrations/multiErr/MultiErrRunner.php
index af74e7d4c..db40bbff4 100644
--- a/App/Database/migrations/multiErr/MultiErrRunner.php
+++ b/App/Database/migrations/multiErr/MultiErrRunner.php
@@ -6,7 +6,6 @@
class MultiErrRunner extends SchemaRunner {
-
public function __construct() {
$conn = new ConnectionInfo('mssql', SQL_SERVER_USER, SQL_SERVER_PASS, SQL_SERVER_DB, SQL_SERVER_HOST, 1433, [
'TrustServerCertificate' => 'true'
diff --git a/App/Database/migrations/noConn/Migration000.php b/App/Database/migrations/noConn/Migration000.php
index 2d463f004..49ffc83bc 100644
--- a/App/Database/migrations/noConn/Migration000.php
+++ b/App/Database/migrations/noConn/Migration000.php
@@ -14,19 +14,19 @@ public function __construct() {
parent::__construct('Third One', 2);
}
/**
- * Performs the action that will apply the migration.
- *
+ * Performs the action that will revert back the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function up(Database $schema) {
- $this->x();
+ public function down(Database $schema) {
+ $schema->y();
}
/**
- * Performs the action that will revert back the migration.
- *
+ * Performs the action that will apply the migration.
+ *
* @param Database $schema The database at which the migration will be applied to.
*/
- public function down(Database $schema) {
- $schema->y();
+ public function up(Database $schema) {
+ $this->x();
}
}
diff --git a/App/Database/migrations/noConn/XRunner.php b/App/Database/migrations/noConn/XRunner.php
index 44715da99..ffbaa574f 100644
--- a/App/Database/migrations/noConn/XRunner.php
+++ b/App/Database/migrations/noConn/XRunner.php
@@ -5,7 +5,6 @@
class XRunner extends SchemaRunner {
-
public function __construct() {
parent::__construct(null);
}
diff --git a/php_cs.php.dist b/php_cs.php.dist
index 3c2e444cc..e9667f771 100644
--- a/php_cs.php.dist
+++ b/php_cs.php.dist
@@ -10,7 +10,7 @@ return $config->setRules([
'align_multiline_comment' => [
'comment_type' => 'phpdocs_only'
],
- 'array_indentation' => [],
+ 'array_indentation' => true,
'array_syntax' => [
'syntax' => 'short'
],
diff --git a/public/index.php b/public/index.php
index a671e5c9b..17c383251 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,5 +1,4 @@
setDescription('');
$this->setRequestMethods([
- RequestMethod::GET,
- RequestMethod::POST,
- RequestMethod::PATCH,
- RequestMethod::HEAD
+ RequestMethod::GET,
+ RequestMethod::POST,
+ RequestMethod::PATCH,
+ RequestMethod::HEAD
]);
$this->addParameters([
'first-name' => [
diff --git a/tests/Apis/Multiple/WebService01.php b/tests/Apis/Multiple/WebService01.php
index 75786a123..3a5ba2449 100644
--- a/tests/Apis/Multiple/WebService01.php
+++ b/tests/Apis/Multiple/WebService01.php
@@ -16,9 +16,8 @@ public function __construct() {
parent::__construct('say-hi-service-2');
$this->setDescription('');
$this->setRequestMethods([
- RequestMethod::HEAD,
+ RequestMethod::HEAD,
]);
-
}
/**
* Checks if the client is authorized to call a service or not.
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
index 6734208ab..78f68f6aa 100644
--- a/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
@@ -48,7 +48,7 @@ public function testAddDBConnection00() {
*/
public function testAddDBConnection01() {
$connName = 'db-connection-'.(count(App::getConfig()->getDBConnections()) + 1);
-
+
$output = $this->executeSingleCommand(new AddDbConnectionCommand(), [
'WebFiori',
'add:db-connection'
@@ -65,7 +65,7 @@ public function testAddDBConnection01() {
$this->assertEquals(0, $this->getExitCode());
$output = $this->getOutput();
-
+
$this->assertEquals("Select database type:\n", $output[0]);
$this->assertEquals("0: mysql\n", $output[1]);
$this->assertEquals("1: mssql\n", $output[2]);
@@ -88,7 +88,7 @@ public function testAddDBConnection01() {
public function testAddDBConnection02() {
$count = count(App::getConfig()->getDBConnections());
$connName = 'db-connection-'.($count + 1);
-
+
$output = $this->executeSingleCommand(new AddDbConnectionCommand(), [
'WebFiori',
'add:db-connection'
@@ -105,7 +105,7 @@ public function testAddDBConnection02() {
$this->assertEquals(0, $this->getExitCode());
$output = $this->getOutput();
-
+
$this->assertEquals("Select database type:\n", $output[0]);
$this->assertEquals("0: mysql\n", $output[1]);
$this->assertEquals("1: mssql\n", $output[2]);
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php
index a78496880..b60c0b550 100644
--- a/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php
@@ -1,7 +1,6 @@
removeClass('\\App\\Langs\\Lang' . $langCode);
+ if (class_exists('\\App\\Langs\\Lang'.$langCode)) {
+ $this->removeClass('\\App\\Langs\\Lang'.$langCode);
}
-
+
$output = $this->executeSingleCommand(new AddLangCommand(), [], [
$langCode,
'F Name',
@@ -45,8 +44,8 @@ public function testAddLang00() {
"1: rtl\n",
"Success: Language added. Also, a class for the language is created at \"".APP_DIR."\Langs\" for that language.\n"
], $output);
- $this->assertTrue(class_exists('\\App\\Langs\\Lang' . $langCode));
- $this->removeClass('\\App\\Langs\\Lang' . $langCode);
+ $this->assertTrue(class_exists('\\App\\Langs\\Lang'.$langCode));
+ $this->removeClass('\\App\\Langs\\Lang'.$langCode);
Controller::getDriver()->initialize();
}
/**
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
index 232e7fc8d..1b8784fde 100644
--- a/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
@@ -16,7 +16,7 @@ class AddSmtpConnectionCommandTest extends CLITestCase {
*/
public function testAddSMTPConnection00() {
$connName = 'smtp-connection-'.count(App::getConfig()->getSMTPConnections());
-
+
$output = $this->executeSingleCommand(new AddSmtpConnectionCommand(), [
'WebFiori',
'add:smtp-connection'
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php
index a9e3a95e4..6b51f926e 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php
@@ -15,7 +15,7 @@ class CreateCommandCommandTest extends CLITestCase {
*/
public function testCreateCommand00() {
$className = 'TestCmd'.time();
-
+
$output = $this->executeSingleCommand(new CreateCommandCommand(), [], [
$className,
"\n", // Use default command name
@@ -31,7 +31,7 @@ public function testCreateCommand00() {
"Add arguments to the command?(y/N)\n",
"Success: Command class created at: ".APP_PATH."Commands".DIRECTORY_SEPARATOR.$className."Command.php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Commands\\'.$className.'Command'));
$this->removeClass('\\App\\Commands\\'.$className.'Command');
}
@@ -40,7 +40,7 @@ public function testCreateCommand00() {
*/
public function testCreateCommand01() {
$className = 'TestCmd'.time();
-
+
$output = $this->executeSingleCommand(new CreateCommandCommand(), [], [
$className,
'test-command',
@@ -66,7 +66,7 @@ public function testCreateCommand01() {
*/
public function testCreateCommand02() {
$className = 'TestCmd'.time();
-
+
$output = $this->executeSingleCommand(new CreateCommandCommand(), [], [
'', // Empty class name - will be rejected
$className, // Valid class name
@@ -85,7 +85,7 @@ public function testCreateCommand02() {
"Add arguments to the command?(y/N)\n",
"Success: Command class created at: ".APP_PATH."Commands".DIRECTORY_SEPARATOR.$className."Command.php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Commands\\'.$className.'Command'));
$this->removeClass('\\App\\Commands\\'.$className.'Command');
}
@@ -94,7 +94,7 @@ public function testCreateCommand02() {
*/
public function testCreateCommandWithArgs00() {
$className = 'TestCmd'.time();
-
+
$output = $this->executeMultiCommand([
CreateCommandCommand::class,
'--class-name' => $className,
@@ -104,7 +104,7 @@ public function testCreateCommandWithArgs00() {
$this->assertEquals(0, $this->getExitCode());
$this->assertContains("Success: Command class created at: ".APP_PATH."Commands".DIRECTORY_SEPARATOR.$className."Command.php\n", $output);
-
+
$this->assertTrue(class_exists('\\App\\Commands\\'.$className.'Command'));
$this->removeClass('\\App\\Commands\\'.$className.'Command');
}
@@ -117,7 +117,7 @@ public function testCreateCommandWithArgs01() {
['name' => '--name', 'description' => 'User name', 'optional' => false],
['name' => '--type', 'description' => 'User type', 'optional' => true, 'values' => ['admin', 'user']]
]);
-
+
$output = $this->executeMultiCommand([
CreateCommandCommand::class,
'--class-name' => $className,
@@ -149,7 +149,7 @@ public function testCreateCommandWithArgs02() {
*/
public function testCreateCommandWithArgs03() {
$className = 'TestCmd'.time();
-
+
$output = $this->executeMultiCommand([
CreateCommandCommand::class,
'--class-name' => $className,
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php
index 9884b55d9..ed9200b26 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php
@@ -15,7 +15,7 @@ class CreateEntityCommandTest extends CLITestCase {
*/
public function testCreateEntity00() {
$className = 'TestEntity'.time();
-
+
$output = $this->executeSingleCommand(new CreateEntityCommand(), [], [
$className,
'n' // Don't add properties
@@ -27,7 +27,7 @@ public function testCreateEntity00() {
"Add properties to the entity?(y/N)\n",
"Success: Entity class created at: ".APP_PATH."Domain".DIRECTORY_SEPARATOR.$className.".php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Domain\\'.$className));
$this->removeClass('\\App\\Domain\\'.$className);
}
@@ -36,7 +36,7 @@ public function testCreateEntity00() {
*/
public function testCreateEntity01() {
$className = 'TestEntity'.time();
-
+
$output = $this->executeSingleCommand(new CreateEntityCommand(), [], [
$className,
'y', // Add properties
@@ -61,7 +61,7 @@ public function testCreateEntity01() {
*/
public function testCreateEntity02() {
$className = 'TestEntity'.time();
-
+
$output = $this->executeSingleCommand(new CreateEntityCommand(), [], [
'', // Empty class name - will be rejected
$className, // Valid class name
@@ -76,7 +76,7 @@ public function testCreateEntity02() {
"Add properties to the entity?(y/N)\n",
"Success: Entity class created at: ".APP_PATH."Domain".DIRECTORY_SEPARATOR.$className.".php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Domain\\'.$className));
$this->removeClass('\\App\\Domain\\'.$className);
}
@@ -85,7 +85,7 @@ public function testCreateEntity02() {
*/
public function testCreateEntityWithArgs00() {
$className = 'TestEntity'.time();
-
+
$output = $this->executeMultiCommand([
CreateEntityCommand::class,
'--class-name' => $className
@@ -93,7 +93,7 @@ public function testCreateEntityWithArgs00() {
$this->assertEquals(0, $this->getExitCode());
$this->assertContains("Success: Entity class created at: ".APP_PATH."Domain".DIRECTORY_SEPARATOR.$className.".php\n", $output);
-
+
$this->assertTrue(class_exists('\\App\\Domain\\'.$className));
$this->removeClass('\\App\\Domain\\'.$className);
}
@@ -107,7 +107,7 @@ public function testCreateEntityWithArgs01() {
['name' => 'name', 'type' => 'string', 'nullable' => false],
['name' => 'email', 'type' => 'string', 'nullable' => true]
]);
-
+
$output = $this->executeMultiCommand([
CreateEntityCommand::class,
'--class-name' => $className,
@@ -135,7 +135,7 @@ public function testCreateEntityWithArgs02() {
*/
public function testCreateEntityWithArgs03() {
$className = 'TestEntity'.time();
-
+
$output = $this->executeMultiCommand([
CreateEntityCommand::class,
'--class-name' => $className,
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
index c12f7123d..2bf49bdd6 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
@@ -15,7 +15,7 @@ class CreateMiddlewareCommandTest extends CLITestCase {
*/
public function testCreateMiddleware00() {
$className = 'TestMd'.time();
-
+
$output = $this->executeSingleCommand(new CreateMiddlewareCommand(), [], [
$className,
"\n", // Use default middleware name (same as class name)
@@ -31,7 +31,7 @@ public function testCreateMiddleware00() {
"Add middleware to groups?(y/N)\n",
"Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
}
@@ -40,7 +40,7 @@ public function testCreateMiddleware00() {
*/
public function testCreateMiddleware01() {
$className = 'TestMd'.time();
-
+
$output = $this->executeSingleCommand(new CreateMiddlewareCommand(), [], [
$className,
'My Custom Middleware',
@@ -53,7 +53,7 @@ public function testCreateMiddleware01() {
$this->assertEquals(0, $this->getExitCode());
$output = $this->getOutput();
-
+
$this->assertEquals("Enter middleware class name:\n", $output[0]);
$this->assertEquals("Enter middleware name: Enter = '$className'\n", $output[1]);
$this->assertEquals("Enter middleware priority: Enter = '0'\n", $output[2]);
@@ -62,7 +62,7 @@ public function testCreateMiddleware01() {
$this->assertEquals("Enter group name (leave empty to finish):\n", $output[5]);
$this->assertEquals("Enter group name (leave empty to finish):\n", $output[6]);
$this->assertEquals("Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n", $output[7]);
-
+
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
}
@@ -71,7 +71,7 @@ public function testCreateMiddleware01() {
*/
public function testCreateMiddleware02() {
$className = 'TestMd'.time();
-
+
$output = $this->executeSingleCommand(new CreateMiddlewareCommand(), [], [
'', // Empty class name - will be rejected
$className, // Valid class name
@@ -90,7 +90,7 @@ public function testCreateMiddleware02() {
"Add middleware to groups?(y/N)\n",
"Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
}
@@ -99,7 +99,7 @@ public function testCreateMiddleware02() {
*/
public function testCreateMiddleware03() {
$className = 'TestMd'.time();
-
+
$output = $this->executeSingleCommand(new CreateMiddlewareCommand(), [
'WebFiori',
'create:middleware'
@@ -112,7 +112,7 @@ public function testCreateMiddleware03() {
$this->assertEquals(0, $this->getExitCode());
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
-
+
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
}
/**
@@ -120,7 +120,7 @@ public function testCreateMiddleware03() {
*/
public function testCreateMiddlewareWithArgs00() {
$className = 'TestMd'.time();
-
+
$output = $this->executeMultiCommand([
CreateMiddlewareCommand::class,
'--class-name' => $className,
@@ -131,7 +131,7 @@ public function testCreateMiddlewareWithArgs00() {
$this->assertEquals(0, $this->getExitCode());
$this->assertContains("Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n", $output);
-
+
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
}
@@ -140,7 +140,7 @@ public function testCreateMiddlewareWithArgs00() {
*/
public function testCreateMiddlewareWithArgs01() {
$className = 'TestMd'.time();
-
+
$output = $this->executeMultiCommand([
CreateMiddlewareCommand::class,
'--class-name' => $className,
@@ -170,7 +170,7 @@ public function testCreateMiddlewareWithArgs02() {
*/
public function testCreateMiddlewareWithArgs03() {
$className = 'TestMd'.time();
-
+
$output = $this->executeMultiCommand([
CreateMiddlewareCommand::class,
'--class-name' => $className,
@@ -187,7 +187,7 @@ public function testCreateMiddlewareWithArgs03() {
*/
public function testCreateMiddlewareWithArgs04() {
$className = 'TestMd'.time();
-
+
$output = $this->executeMultiCommand([
CreateMiddlewareCommand::class,
'--class-name' => $className,
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php
index b9a63ff79..8b0ddf450 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php
@@ -15,7 +15,7 @@ class CreateRepositoryCommandTest extends CLITestCase {
*/
public function testCreateRepository00() {
$className = 'TestRepo'.time();
-
+
$output = $this->executeSingleCommand(new CreateRepositoryCommand(), [], [
$className,
'App\\Domain\\User',
@@ -33,7 +33,7 @@ public function testCreateRepository00() {
"Add properties to the repository?(y/N)\n",
"Success: Repository class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Repository".DIRECTORY_SEPARATOR.$className.".php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Infrastructure\\Repository\\'.$className));
$this->removeClass('\\App\\Infrastructure\\Repository\\'.$className);
}
@@ -42,7 +42,7 @@ public function testCreateRepository00() {
*/
public function testCreateRepository01() {
$className = 'TestRepo'.time();
-
+
$output = $this->executeSingleCommand(new CreateRepositoryCommand(), [], [
'', // Empty class name - will be rejected
$className, // Valid class name
@@ -63,7 +63,7 @@ public function testCreateRepository01() {
"Add properties to the repository?(y/N)\n",
"Success: Repository class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Repository".DIRECTORY_SEPARATOR.$className.".php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Infrastructure\\Repository\\'.$className));
$this->removeClass('\\App\\Infrastructure\\Repository\\'.$className);
}
@@ -72,7 +72,7 @@ public function testCreateRepository01() {
*/
public function testCreateRepositoryWithArgs00() {
$className = 'TestRepo'.time();
-
+
$output = $this->executeMultiCommand([
CreateRepositoryCommand::class,
'--class-name' => $className,
@@ -83,7 +83,7 @@ public function testCreateRepositoryWithArgs00() {
$this->assertEquals(0, $this->getExitCode());
$this->assertContains("Success: Repository class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Repository".DIRECTORY_SEPARATOR.$className.".php\n", $output);
-
+
$this->assertTrue(class_exists('\\App\\Infrastructure\\Repository\\'.$className));
$this->removeClass('\\App\\Infrastructure\\Repository\\'.$className);
}
@@ -97,7 +97,7 @@ public function testCreateRepositoryWithArgs01() {
['name' => 'name', 'type' => 'string'],
['name' => 'email', 'type' => 'string']
]);
-
+
$output = $this->executeMultiCommand([
CreateRepositoryCommand::class,
'--class-name' => $className,
@@ -130,7 +130,7 @@ public function testCreateRepositoryWithArgs02() {
*/
public function testCreateRepositoryWithArgs03() {
$className = 'TestRepo'.time();
-
+
$output = $this->executeMultiCommand([
CreateRepositoryCommand::class,
'--class-name' => $className,
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php
index 1fc9f4fe0..1ea2f0108 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php
@@ -15,7 +15,7 @@ class CreateServiceCommandTest extends CLITestCase {
*/
public function testCreateService00() {
$className = 'TestService'.time();
-
+
$output = $this->executeSingleCommand(new CreateServiceCommand(), [], [
$className,
"\n", // Use default description
@@ -29,7 +29,7 @@ public function testCreateService00() {
"Add methods to the service?(y/N)\n",
"Success: Service class created at: ".APP_PATH."Apis".DIRECTORY_SEPARATOR.$className."Service.php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Apis\\'.$className.'Service'));
$this->removeClass('\\App\\Apis\\'.$className.'Service');
}
@@ -38,7 +38,7 @@ public function testCreateService00() {
*/
public function testCreateService01() {
$className = 'TestService'.time();
-
+
$output = $this->executeSingleCommand(new CreateServiceCommand(), [], [
'', // Empty class name - will be rejected
$className, // Valid class name
@@ -55,7 +55,7 @@ public function testCreateService01() {
"Add methods to the service?(y/N)\n",
"Success: Service class created at: ".APP_PATH."Apis".DIRECTORY_SEPARATOR.$className."Service.php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Apis\\'.$className.'Service'));
$this->removeClass('\\App\\Apis\\'.$className.'Service');
}
@@ -64,7 +64,7 @@ public function testCreateService01() {
*/
public function testCreateServiceWithArgs00() {
$className = 'TestService'.time();
-
+
$output = $this->executeMultiCommand([
CreateServiceCommand::class,
'--class-name' => $className,
@@ -73,7 +73,7 @@ public function testCreateServiceWithArgs00() {
$this->assertEquals(0, $this->getExitCode());
$this->assertContains("Success: Service class created at: ".APP_PATH."Apis".DIRECTORY_SEPARATOR.$className."Service.php\n", $output);
-
+
$this->assertTrue(class_exists('\\App\\Apis\\'.$className.'Service'));
$this->removeClass('\\App\\Apis\\'.$className.'Service');
}
@@ -101,7 +101,7 @@ public function testCreateServiceWithArgs01() {
'return' => 'array'
]
]);
-
+
$output = $this->executeMultiCommand([
CreateServiceCommand::class,
'--class-name' => $className,
@@ -131,7 +131,7 @@ public function testCreateServiceWithArgs02() {
*/
public function testCreateServiceWithArgs03() {
$className = 'TestService'.time();
-
+
$output = $this->executeMultiCommand([
CreateServiceCommand::class,
'--class-name' => $className,
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
index 4e73c0877..7c74b4540 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
@@ -15,7 +15,7 @@ class CreateTableCommandTest extends CLITestCase {
*/
public function testCreateTable00() {
$className = 'TestTable'.time();
-
+
$output = $this->executeSingleCommand(new CreateTableCommand(), [], [
$className,
"\n", // Use default table name
@@ -29,7 +29,7 @@ public function testCreateTable00() {
"Add columns to the table?(y/N)\n",
"Success: Table class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Schema".DIRECTORY_SEPARATOR.$className.".php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
$this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
}
@@ -38,7 +38,7 @@ public function testCreateTable00() {
*/
public function testCreateTable01() {
$className = 'TestTable'.time();
-
+
$output = $this->executeSingleCommand(new CreateTableCommand(), [], [
'', // Empty class name - will be rejected
$className, // Valid class name
@@ -55,7 +55,7 @@ public function testCreateTable01() {
"Add columns to the table?(y/N)\n",
"Success: Table class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Schema".DIRECTORY_SEPARATOR.$className.".php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
$this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
}
@@ -64,7 +64,7 @@ public function testCreateTable01() {
*/
public function testCreateTableWithArgs00() {
$className = 'TestTable'.time();
-
+
$output = $this->executeMultiCommand([
CreateTableCommand::class,
'--class-name' => $className,
@@ -73,7 +73,7 @@ public function testCreateTableWithArgs00() {
$this->assertEquals(0, $this->getExitCode());
$this->assertContains("Success: Table class created at: ".APP_PATH."Infrastructure".DIRECTORY_SEPARATOR."Schema".DIRECTORY_SEPARATOR.$className.".php\n", $output);
-
+
$this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
$this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
}
@@ -87,7 +87,7 @@ public function testCreateTableWithArgs01() {
['name' => 'name', 'type' => 'VARCHAR', 'size' => 255, 'nullable' => false],
['name' => 'email', 'type' => 'VARCHAR', 'size' => 255, 'nullable' => true]
]);
-
+
$output = $this->executeMultiCommand([
CreateTableCommand::class,
'--class-name' => $className,
@@ -117,7 +117,7 @@ public function testCreateTableWithArgs02() {
*/
public function testCreateTableWithArgs03() {
$className = 'TestTable'.time();
-
+
$output = $this->executeMultiCommand([
CreateTableCommand::class,
'--class-name' => $className,
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php
index bf10e9804..b3b8a27a8 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php
@@ -15,7 +15,7 @@ class CreateTaskCommandTest extends CLITestCase {
*/
public function testCreateTask00() {
$className = 'TestTask'.time();
-
+
$output = $this->executeSingleCommand(new CreateTaskCommand(), [], [
$className,
"\n", // Use default task name (same as class name)
@@ -31,7 +31,7 @@ public function testCreateTask00() {
"Add execution arguments to the task?(y/N)\n",
"Success: Task class created at: ".APP_PATH."Tasks".DIRECTORY_SEPARATOR.$className."Task.php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
$this->removeClass('\\App\\Tasks\\'.$className.'Task');
}
@@ -40,7 +40,7 @@ public function testCreateTask00() {
*/
public function testCreateTask01() {
$className = 'TestTask'.time();
-
+
$output = $this->executeSingleCommand(new CreateTaskCommand(), [], [
$className,
'Email Sender Task',
@@ -64,7 +64,7 @@ public function testCreateTask01() {
*/
public function testCreateTask02() {
$className = 'TestTask'.time();
-
+
$output = $this->executeSingleCommand(new CreateTaskCommand(), [], [
'', // Empty class name - will be rejected
$className, // Valid class name
@@ -83,7 +83,7 @@ public function testCreateTask02() {
"Add execution arguments to the task?(y/N)\n",
"Success: Task class created at: ".APP_PATH."Tasks".DIRECTORY_SEPARATOR.$className."Task.php\n"
], $output);
-
+
$this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
$this->removeClass('\\App\\Tasks\\'.$className.'Task');
}
@@ -92,7 +92,7 @@ public function testCreateTask02() {
*/
public function testCreateTask03() {
$className = 'TestTask'.time();
-
+
$output = $this->executeSingleCommand(new CreateTaskCommand(), [
'WebFiori',
'create:task'
@@ -112,7 +112,7 @@ public function testCreateTask03() {
*/
public function testCreateTaskWithArgs00() {
$className = 'TestTask'.time();
-
+
$output = $this->executeMultiCommand([
CreateTaskCommand::class,
'--class-name' => $className,
@@ -122,7 +122,7 @@ public function testCreateTaskWithArgs00() {
$this->assertEquals(0, $this->getExitCode());
$this->assertContains("Success: Task class created at: ".APP_PATH."Tasks".DIRECTORY_SEPARATOR.$className."Task.php\n", $output);
-
+
$this->assertTrue(class_exists('\\App\\Tasks\\'.$className.'Task'));
$this->removeClass('\\App\\Tasks\\'.$className.'Task');
}
@@ -131,7 +131,7 @@ public function testCreateTaskWithArgs00() {
*/
public function testCreateTaskWithArgs01() {
$className = 'TestTask'.time();
-
+
$output = $this->executeMultiCommand([
CreateTaskCommand::class,
'--class-name' => $className,
@@ -166,7 +166,7 @@ public function testCreateTaskWithArgs03() {
['name' => 'email', 'description' => 'Email address', 'default' => 'admin@example.com'],
['name' => 'subject', 'description' => 'Email subject']
]);
-
+
$output = $this->executeMultiCommand([
CreateTaskCommand::class,
'--class-name' => $className,
@@ -184,7 +184,7 @@ public function testCreateTaskWithArgs03() {
*/
public function testCreateTaskWithArgs04() {
$className = 'TestTask'.time();
-
+
$output = $this->executeMultiCommand([
CreateTaskCommand::class,
'--class-name' => $className,
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 1a1b00abf..501cc79ae 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -11,7 +11,7 @@ class HelpCommandTest extends CLITestCase {
*/
public function test00() {
$this->assertEquals([
- "WebFiori Framework (c) Version ". WF_VERSION." ".WF_VERSION_TYPE."\n\n\n",
+ "WebFiori Framework (c) Version ".WF_VERSION." ".WF_VERSION_TYPE."\n\n\n",
"Usage:\n",
" command [arg1 arg2=\"val\" arg3...]\n\n",
"Global Arguments:\n",
diff --git a/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php
index 7c6d38698..4ca0610ef 100644
--- a/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php
@@ -8,56 +8,32 @@
/**
* Test cases for RunMigrationsCommand.
- *
+ *
* @author Ibrahim
*/
class RunMigrationsCommandTest extends CLITestCase {
-
private ConnectionInfo $testConnection;
-
- protected function setUp(): void {
- parent::setUp();
- $this->setupTestConnection();
- $this->cleanupMigrations();
- }
-
- protected function tearDown(): void {
- $this->cleanupMigrations();
- App::getConfig()->removeAllDBConnections();
- parent::tearDown();
- }
-
- private function setupTestConnection(): void {
- $this->testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
- $this->testConnection->setName('test-connection');
- App::getConfig()->addOrUpdateDBConnection($this->testConnection);
- }
-
- private function cleanupMigrations(): void {
- $dir = APP_PATH . 'Database' . DS . 'Migrations';
- if (is_dir($dir)) {
- foreach (glob($dir . DS . '*.php') as $file) {
- if (basename($file) !== '.gitkeep') {
- unlink($file);
- }
- }
- }
- }
-
+
/**
* @test
*/
- public function testExecWithNoConnections(): void {
- App::getConfig()->removeAllDBConnections();
-
+ public function testDryRun(): void {
+ // Create a test migration
+ $this->createTestMigration('TestMigration');
+
$output = $this->executeMultiCommand([
- RunMigrationsCommand::class
+ RunMigrationsCommand::class,
+ '--connection' => 'test-connection',
+ '--dry-run'
]);
-
- $this->assertContains("Info: No database connections configured.\n", $output);
- $this->assertEquals(1, $this->getExitCode());
+
+ // Check if output contains expected text
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Pending migrations:', $outputStr);
+ $this->assertStringContainsString('TestMigration', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
}
-
+
/**
* @test
*/
@@ -66,11 +42,25 @@ public function testExecWithInvalidConnection(): void {
RunMigrationsCommand::class,
'--connection' => 'invalid-connection'
]);
-
+
$this->assertContains("Error: Connection 'invalid-connection' not found.\n", $output);
$this->assertEquals(1, $this->getExitCode());
}
-
+
+ /**
+ * @test
+ */
+ public function testExecWithNoConnections(): void {
+ App::getConfig()->removeAllDBConnections();
+
+ $output = $this->executeMultiCommand([
+ RunMigrationsCommand::class
+ ]);
+
+ $this->assertContains("Info: No database connections configured.\n", $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
/**
* @test
*/
@@ -80,12 +70,12 @@ public function testInitializeMigrationsTable(): void {
'--connection' => 'test-connection',
'--init'
]);
-
+
$this->assertContains("Creating migrations tracking table...\n", $output);
$this->assertContains("Success: Migrations table created successfully.\n", $output);
$this->assertEquals(0, $this->getExitCode());
}
-
+
/**
* @test
*/
@@ -94,37 +84,30 @@ public function testRunWithNoMigrations(): void {
RunMigrationsCommand::class,
'--connection' => 'test-connection'
]);
-
+
$this->assertContains("Info: No migrations found.\n", $output);
$this->assertEquals(0, $this->getExitCode());
}
-
- /**
- * @test
- */
- public function testDryRun(): void {
- // Create a test migration
- $this->createTestMigration('TestMigration');
-
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection',
- '--dry-run'
- ]);
-
- // Check if output contains expected text
- $outputStr = implode('', $output);
- $this->assertStringContainsString('Pending migrations:', $outputStr);
- $this->assertStringContainsString('TestMigration', $outputStr);
- $this->assertEquals(0, $this->getExitCode());
+
+ private function cleanupMigrations(): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (is_dir($dir)) {
+ foreach (glob($dir.DS.'*.php') as $file) {
+ if (basename($file) !== '.gitkeep') {
+ unlink($file);
+ }
+ }
+ }
}
-
+
private function createTestMigration(string $name): void {
- $dir = APP_PATH . 'Database' . DS . 'Migrations';
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
-
+
$content = <<testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
+ $this->testConnection->setName('test-connection');
+ App::getConfig()->addOrUpdateDBConnection($this->testConnection);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->setupTestConnection();
+ $this->cleanupMigrations();
+ }
+
+ protected function tearDown(): void {
+ $this->cleanupMigrations();
+ App::getConfig()->removeAllDBConnections();
+ parent::tearDown();
}
}
diff --git a/tests/WebFiori/Framework/Tests/Cli/SchedulerCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/SchedulerCommandTest.php
index 9bb2d5fdb..b1f83684c 100644
--- a/tests/WebFiori/Framework/Tests/Cli/SchedulerCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/SchedulerCommandTest.php
@@ -17,27 +17,38 @@ public function setUp() : void {
TasksManager::setPassword('123456');
TasksManager::registerTasks();
}
+
/**
* @test
*/
- public function testRunWithoutRequiredOptions() {
+ public function testCancelTaskSelection() {
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
- ], []);
+ '--force',
+ 'p' => '123456'
+ ], [
+ '5'
+ ]);
- $this->assertEquals(-1, $this->getExitCode());
+ $this->assertEquals(0, $this->getExitCode());
$this->assertEquals([
- "Info: At least one of the options '--check', '--force' or '--show-task-args' must be provided.\n"
+ "Select one of the scheduled tasks to force:\n",
+ "0: Fail 1\n",
+ "1: Fail 2\n",
+ "2: Fail 3\n",
+ "3: Success Every Minute\n",
+ "4: Success 1\n",
+ "5: Cancel <--\n",
], $output);
}
-
+
/**
* @test
*/
public function testCheckScheduledTasks() {
TasksManager::setPassword('123456');
-
+
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
@@ -57,7 +68,7 @@ public function testCheckScheduledTasks() {
" Fail 3\n",
], $output);
}
-
+
/**
* @test
*/
@@ -73,12 +84,95 @@ public function testCheckWithoutPassword() {
"Error: The argument 'p' is missing. It must be provided if scheduler password is set.\n",
], $output);
}
-
+
+ /**
+ * @test
+ */
+ public function testCheckWithValidPassword() {
+ TasksManager::setPassword('123456');
+ $output = $this->executeSingleCommand(new SchedulerCommand(), [
+ 'WebFiori',
+ 'scheduler',
+ '--check',
+ 'p' => '123456'
+ ], []);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Total number of tasks: 5\n",
+ "Executed Tasks: 4\n",
+ "Successfully finished tasks:\n",
+ " Success Every Minute\n",
+ "Failed tasks:\n",
+ " Fail 1\n",
+ " Fail 2\n",
+ " Fail 3\n",
+ ], $output);
+ }
+
+ /**
+ * @test
+ */
+ public function testForceNonExistentTask() {
+ $output = $this->executeSingleCommand(new SchedulerCommand(), [
+ 'WebFiori',
+ 'scheduler',
+ '--force',
+ '--task-name="Rand"',
+ 'p' => '123456'
+ ], [
+ 'Hell',
+ '5'
+ ]);
+
+ $this->assertEquals(-1, $this->getExitCode());
+ $this->assertEquals([
+ "Error: No task was found which has the name 'Rand'\n",
+ ], $output);
+ }
+
+ /**
+ * @test
+ */
+ public function testForceSpecificTaskByName() {
+ $this->getRunner(true);
+ $output = $this->executeSingleCommand(new SchedulerCommand(), [
+ 'WebFiori',
+ 'scheduler',
+ '--force',
+ '--show-log',
+ '--task-name' => 'Success 1',
+ 'p' => '123456'
+ ], [
+ 'N'
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals([
+ "Would you like to customize execution arguments?(y/N)\n",
+ "Running task(s) check...\n",
+ "Forcing task 'Success 1' to execute...\n",
+ "Active task: \"Success 1\" ...\n",
+ "Calling the method App\\Tasks\SuccessTestTask::execute()\n",
+ "Start: 2021-07-08\n",
+ "End: \n",
+ "The task was forced.\n",
+ "Calling the method App\\Tasks\SuccessTestTask::onSuccess()\n",
+ "Calling the method App\\Tasks\SuccessTestTask::afterExec()\n",
+ "Check finished.\n",
+ "Total number of tasks: 5\n",
+ "Executed Tasks: 1\n",
+ "Successfully finished tasks:\n",
+ " Success 1\n",
+ "Failed tasks:\n",
+ " \n"
+ ], $output);
+ }
+
/**
* @test
*/
public function testForceTaskExecution() {
-
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
@@ -105,48 +199,48 @@ public function testForceTaskExecution() {
" Fail 1\n"
], $output);
}
-
+
/**
* @test
*/
- public function testForceTaskWithLogging() {
+ public function testForceTaskWithCustomArguments() {
+ TasksManager::execLog(true);
+ $this->getRunner(true);
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
'--force',
'--show-log',
+ '--task-name' => 'Success 1',
+ 'start' => '2021',
+ 'end' => '2022',
'p' => '123456'
], [
- '0'
+ 'N'
]);
$this->assertEquals(0, $this->getExitCode());
$this->assertEquals([
- "Select one of the scheduled tasks to force:\n",
- "0: Fail 1\n",
- "1: Fail 2\n",
- "2: Fail 3\n",
- "3: Success Every Minute\n",
- "4: Success 1\n",
- "5: Cancel <--\n",
+ "Would you like to customize execution arguments?(y/N)\n",
"Running task(s) check...\n",
- "Forcing task 'Fail 1' to execute...\n",
- "Active task: \"Fail 1\" ...\n",
- "Calling the method App\\Tasks\Fail1TestTask::execute()\n",
- "Info: Task Fail 1 Is executing...\n",
- "Calling the method App\\Tasks\Fail1TestTask::onFail()\n",
- "Error: Task Fail 1 Failed.\n",
- "Calling the method App\\Tasks\Fail1TestTask::afterExec()\n",
+ "Forcing task 'Success 1' to execute...\n",
+ "Active task: \"Success 1\" ...\n",
+ "Calling the method App\\Tasks\SuccessTestTask::execute()\n",
+ "Start: 2021\n",
+ "End: 2022\n",
+ "The task was forced.\n",
+ "Calling the method App\\Tasks\SuccessTestTask::onSuccess()\n",
+ "Calling the method App\\Tasks\SuccessTestTask::afterExec()\n",
"Check finished.\n",
"Total number of tasks: 5\n",
"Executed Tasks: 1\n",
"Successfully finished tasks:\n",
- " \n",
+ " Success 1\n",
"Failed tasks:\n",
- " Fail 1\n"
+ " \n"
], $output);
}
-
+
/**
* @test
*/
@@ -194,7 +288,7 @@ public function testForceTaskWithExceptionLogging() {
"#11 At class WebFiori\\Cli\\CommandTestCase Line:",
"Skip"];
$idx = 0;
-
+
foreach ($expected as $item) {
if ($item == 'Skip') {
break;
@@ -203,144 +297,92 @@ public function testForceTaskWithExceptionLogging() {
$idx++;
}
}
-
+
/**
* @test
*/
- public function testForceSpecificTaskByName() {
- $this->getRunner(true);
+ public function testForceTaskWithIncorrectPassword() {
+ TasksManager::reset();
+ TasksManager::execLog(true);
+ TasksManager::setPassword('123456');
+ TasksManager::registerTasks();
+
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
'--force',
- '--show-log',
'--task-name' => 'Success 1',
- 'p' => '123456'
], [
'N'
]);
- $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals(-1, $this->getExitCode());
$this->assertEquals([
"Would you like to customize execution arguments?(y/N)\n",
- "Running task(s) check...\n",
- "Forcing task 'Success 1' to execute...\n",
- "Active task: \"Success 1\" ...\n",
- "Calling the method App\\Tasks\SuccessTestTask::execute()\n",
- "Start: 2021-07-08\n",
- "End: \n",
- "The task was forced.\n",
- "Calling the method App\\Tasks\SuccessTestTask::onSuccess()\n",
- "Calling the method App\\Tasks\SuccessTestTask::afterExec()\n",
- "Check finished.\n",
- "Total number of tasks: 5\n",
- "Executed Tasks: 1\n",
- "Successfully finished tasks:\n",
- " Success 1\n",
- "Failed tasks:\n",
- " \n"
+ "Error: Provided password is incorrect.\n",
], $output);
+ $this->assertEquals([
+ "Running task(s) check...",
+ "Error: Given password is incorrect.",
+ "Check finished.",
+ ], TasksManager::getLogArray());
}
-
+
/**
* @test
*/
- public function testForceTaskWithCustomArguments() {
+ public function testForceTaskWithInteractiveArguments() {
+ TasksManager::reset();
TasksManager::execLog(true);
- $this->getRunner(true);
+ TasksManager::setPassword('123456');
+ TasksManager::registerTasks();
+
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
'--force',
- '--show-log',
'--task-name' => 'Success 1',
- 'start' => '2021',
- 'end' => '2022',
'p' => '123456'
], [
- 'N'
+ 'Y',
+ '2021-01-01',
+ '2020-01-01'
]);
-
- $this->assertEquals(0, $this->getExitCode());
+ // Debug: Print actual output for GitHub Actions
$this->assertEquals([
"Would you like to customize execution arguments?(y/N)\n",
- "Running task(s) check...\n",
- "Forcing task 'Success 1' to execute...\n",
- "Active task: \"Success 1\" ...\n",
- "Calling the method App\\Tasks\SuccessTestTask::execute()\n",
- "Start: 2021\n",
- "End: 2022\n",
+ "Enter a value for the argument \"start\": Enter = ''\n",
+ "Enter a value for the argument \"end\": Enter = ''\n",
+ "Start: 2021-01-01\n",
+ "End: 2020-01-01\n",
"The task was forced.\n",
- "Calling the method App\\Tasks\SuccessTestTask::onSuccess()\n",
- "Calling the method App\\Tasks\SuccessTestTask::afterExec()\n",
- "Check finished.\n",
"Total number of tasks: 5\n",
"Executed Tasks: 1\n",
"Successfully finished tasks:\n",
" Success 1\n",
"Failed tasks:\n",
- " \n"
- ], $output);
- }
-
- /**
- * @test
- */
- public function testForceTaskWithIncorrectPassword() {
- TasksManager::reset();
- TasksManager::execLog(true);
- TasksManager::setPassword('123456');
- TasksManager::registerTasks();
-
- $output = $this->executeSingleCommand(new SchedulerCommand(), [
- 'WebFiori',
- 'scheduler',
- '--force',
- '--task-name' => 'Success 1',
- ], [
- 'N'
- ]);
-
- $this->assertEquals(-1, $this->getExitCode());
- $this->assertEquals([
- "Would you like to customize execution arguments?(y/N)\n",
- "Error: Provided password is incorrect.\n",
+ " \n",
], $output);
$this->assertEquals([
- "Running task(s) check...",
- "Error: Given password is incorrect.",
+ 'Running task(s) check...',
+ "Forcing task 'Success 1' to execute...",
+ "Active task: \"Success 1\" ...",
+ "Calling the method App\\Tasks\SuccessTestTask::execute()",
+ "Calling the method App\\Tasks\SuccessTestTask::onSuccess()",
+ "Calling the method App\\Tasks\SuccessTestTask::afterExec()",
"Check finished.",
], TasksManager::getLogArray());
}
-
- /**
- * @test
- */
- public function testShowTaskArguments() {
- $output = $this->executeSingleCommand(new SchedulerCommand(), [
- 'WebFiori',
- 'scheduler',
- '--task-name' => 'Success 1',
- '--show-task-args',
- 'p' => '123456'
- ], []);
- $this->assertEquals(0, $this->getExitCode());
- $this->assertEquals([
- "Task Args:\n",
- " start: Start date of the report.\n",
- " end: End date of the report.\n",
- ], $output);
- }
-
/**
* @test
*/
- public function testShowTaskArgumentsWithSelection() {
+ public function testForceTaskWithLogging() {
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
- '--show-task-args',
+ '--force',
+ '--show-log',
'p' => '123456'
], [
'0'
@@ -348,17 +390,31 @@ public function testShowTaskArgumentsWithSelection() {
$this->assertEquals(0, $this->getExitCode());
$this->assertEquals([
- "Select one of the scheduled tasks to show supported args:\n",
+ "Select one of the scheduled tasks to force:\n",
"0: Fail 1\n",
"1: Fail 2\n",
"2: Fail 3\n",
"3: Success Every Minute\n",
"4: Success 1\n",
- "Task Args:\n",
- " \n",
+ "5: Cancel <--\n",
+ "Running task(s) check...\n",
+ "Forcing task 'Fail 1' to execute...\n",
+ "Active task: \"Fail 1\" ...\n",
+ "Calling the method App\\Tasks\Fail1TestTask::execute()\n",
+ "Info: Task Fail 1 Is executing...\n",
+ "Calling the method App\\Tasks\Fail1TestTask::onFail()\n",
+ "Error: Task Fail 1 Failed.\n",
+ "Calling the method App\\Tasks\Fail1TestTask::afterExec()\n",
+ "Check finished.\n",
+ "Total number of tasks: 5\n",
+ "Executed Tasks: 1\n",
+ "Successfully finished tasks:\n",
+ " \n",
+ "Failed tasks:\n",
+ " Fail 1\n"
], $output);
}
-
+
/**
* @test
*/
@@ -389,121 +445,64 @@ public function testListAllScheduledTasks() {
"Cron Expression : 30 4 * * *\n",
], $output);
}
-
/**
* @test
*/
- public function testCheckWithValidPassword() {
- TasksManager::setPassword('123456');
+ public function testRunWithoutRequiredOptions() {
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
- '--check',
- 'p' => '123456'
], []);
- $this->assertEquals(0, $this->getExitCode());
+ $this->assertEquals(-1, $this->getExitCode());
$this->assertEquals([
- "Total number of tasks: 5\n",
- "Executed Tasks: 4\n",
- "Successfully finished tasks:\n",
- " Success Every Minute\n",
- "Failed tasks:\n",
- " Fail 1\n",
- " Fail 2\n",
- " Fail 3\n",
+ "Info: At least one of the options '--check', '--force' or '--show-task-args' must be provided.\n"
], $output);
}
-
+
/**
* @test
*/
- public function testForceTaskWithInteractiveArguments() {
- TasksManager::reset();
- TasksManager::execLog(true);
- TasksManager::setPassword('123456');
- TasksManager::registerTasks();
-
+ public function testShowTaskArguments() {
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
- '--force',
'--task-name' => 'Success 1',
+ '--show-task-args',
'p' => '123456'
- ], [
- 'Y',
- '2021-01-01',
- '2020-01-01'
- ]);
- // Debug: Print actual output for GitHub Actions
+ ], []);
+
+ $this->assertEquals(0, $this->getExitCode());
$this->assertEquals([
- "Would you like to customize execution arguments?(y/N)\n",
- "Enter a value for the argument \"start\": Enter = ''\n",
- "Enter a value for the argument \"end\": Enter = ''\n",
- "Start: 2021-01-01\n",
- "End: 2020-01-01\n",
- "The task was forced.\n",
- "Total number of tasks: 5\n",
- "Executed Tasks: 1\n",
- "Successfully finished tasks:\n",
- " Success 1\n",
- "Failed tasks:\n",
- " \n",
+ "Task Args:\n",
+ " start: Start date of the report.\n",
+ " end: End date of the report.\n",
], $output);
- $this->assertEquals([
- 'Running task(s) check...',
- "Forcing task 'Success 1' to execute...",
- "Active task: \"Success 1\" ...",
- "Calling the method App\\Tasks\SuccessTestTask::execute()",
- "Calling the method App\\Tasks\SuccessTestTask::onSuccess()",
- "Calling the method App\\Tasks\SuccessTestTask::afterExec()",
- "Check finished.",
- ], TasksManager::getLogArray());
}
-
+
/**
* @test
*/
- public function testCancelTaskSelection() {
+ public function testShowTaskArgumentsWithSelection() {
$output = $this->executeSingleCommand(new SchedulerCommand(), [
'WebFiori',
'scheduler',
- '--force',
+ '--show-task-args',
'p' => '123456'
], [
- '5'
+ '0'
]);
$this->assertEquals(0, $this->getExitCode());
$this->assertEquals([
- "Select one of the scheduled tasks to force:\n",
+ "Select one of the scheduled tasks to show supported args:\n",
"0: Fail 1\n",
"1: Fail 2\n",
"2: Fail 3\n",
"3: Success Every Minute\n",
"4: Success 1\n",
- "5: Cancel <--\n",
- ], $output);
- }
-
- /**
- * @test
- */
- public function testForceNonExistentTask() {
- $output = $this->executeSingleCommand(new SchedulerCommand(), [
- 'WebFiori',
- 'scheduler',
- '--force',
- '--task-name="Rand"',
- 'p' => '123456'
- ], [
- 'Hell',
- '5'
- ]);
-
- $this->assertEquals(-1, $this->getExitCode());
- $this->assertEquals([
- "Error: No task was found which has the name 'Rand'\n",
+ "Task Args:\n",
+ " \n",
], $output);
}
}
diff --git a/tests/WebFiori/Framework/Tests/Cli/VersionCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/VersionCommandTest.php
index d40a98a80..13195339b 100644
--- a/tests/WebFiori/Framework/Tests/Cli/VersionCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/VersionCommandTest.php
@@ -23,7 +23,7 @@ public function test00() {
'Version Type: '.WF_VERSION_TYPE."\n",
], $output);
}
-
+
/**
* @test
*/
diff --git a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
index 732a41328..e714dbb02 100644
--- a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
+++ b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
@@ -1,12 +1,12 @@
assertEquals('تطبيق', $driver->getAppName('AR'));
$this->assertNull($driver->getAppName('BK'));
}
- /**
- * @test
- */
- public function testSetConfigFileName00() {
- JsonDriver::setConfigFileName('app-config.json');
- $this->assertEquals('app-config', JsonDriver::getConfigFileName());
- JsonDriver::setConfigFileName('super-conf.json');
- $this->assertEquals('super-conf', JsonDriver::getConfigFileName());
- JsonDriver::setConfigFileName('super-confx.kkp');
- $this->assertEquals('super-confx', JsonDriver::getConfigFileName());
- }
- /**
- * @test
- */
- public function testAppNames00() {
- $driver = new JsonDriver();
- $this->assertEquals([
- 'AR' => 'تطبيق',
- 'EN' => 'Application'
- ],$driver->getAppNames());
- $driver->setAppName('Cool App', 'En');
- $this->assertEquals([
- 'AR' => 'تطبيق',
- 'EN' => 'Cool App'
- ],$driver->getAppNames());
- $driver->setAppName('Cool App', 'Enx');
- $this->assertEquals([
- 'AR' => 'تطبيق',
- 'EN' => 'Cool App'
- ],$driver->getAppNames());
- $driver->initialize();
- }
- /**
- * @test
- * @depends testAppNames00
- */
- public function testAppNames01() {
- $driver = new JsonDriver();
- $this->assertEquals([
- 'AR' => 'تطبيق',
- 'EN' => 'Application'
- ],$driver->getAppNames());
- $driver->initialize();
- $this->assertEquals([
- 'AR' => 'تطبيق',
- 'EN' => 'Cool App'
- ],$driver->getAppNames());
- $this->assertTrue(File::isFileExist(JsonDriver::getConfigPath().DS.$driver->getConfigFileName().'.json'));
- $driver->remove();
- $this->assertFalse(File::isFileExist(JsonDriver::getConfigPath().DS.$driver->getConfigFileName().'.json'));
- }
- /**
- * @test
- */
- public function testSetPrimaryLanguage00() {
- $driver = new JsonDriver();
- $this->assertEquals('EN', $driver->getPrimaryLanguage());
- $driver->setPrimaryLanguage('ar');
- }
- /**
- * @test
- * @depends testSetPrimaryLanguage00
- */
- public function testSetPrimaryLanguage01() {
- $driver = new JsonDriver();
- $driver->initialize();
- $this->assertEquals('AR', $driver->getPrimaryLanguage());
- $driver->setPrimaryLanguage('');
- }
- /**
- * @test
- * @depends testSetPrimaryLanguage01
- */
- public function testSetPrimaryLanguage02() {
- $driver = new JsonDriver();
- $driver->initialize();
- $this->assertEquals('AR', $driver->getPrimaryLanguage());
- }
/**
* @test
*/
@@ -224,142 +146,353 @@ public function testAddEnvVar02() {
/**
* @test
*/
- public function testSetTitle00() {
+ public function testAppNames00() {
$driver = new JsonDriver();
$this->assertEquals([
- 'AR' => 'افتراضي',
- 'EN' => 'Default'
- ], $driver->getTitles());
- $this->assertEquals('Default', $driver->getTitle('En'));
- $this->assertEquals('افتراضي', $driver->getTitle('aR'));
- $this->assertEquals('', $driver->getTitle('aRn'));
- $this->assertEquals('', $driver->getTitle(''));
- $driver->setTitle('Ok', 'En');
- $driver->setTitle('اوكي', 'ar');
- }
- /**
- * @test
- * @depends testSetTitle00
- */
- public function testSetTitle01() {
- $driver = new JsonDriver();
- $driver->initialize();
+ 'AR' => 'تطبيق',
+ 'EN' => 'Application'
+ ],$driver->getAppNames());
+ $driver->setAppName('Cool App', 'En');
$this->assertEquals([
- 'AR' => 'اوكي',
- 'EN' => 'Ok'
- ], $driver->getTitles());
- $this->assertEquals('Ok', $driver->getTitle('En'));
- $this->assertEquals('اوكي', $driver->getTitle('aR'));
- $driver->setTitle('Jap', 'Jp');
- $driver->setTitle('Look', 'RUP');
- $driver->setTitle('', 'En');
+ 'AR' => 'تطبيق',
+ 'EN' => 'Cool App'
+ ],$driver->getAppNames());
+ $driver->setAppName('Cool App', 'Enx');
+ $this->assertEquals([
+ 'AR' => 'تطبيق',
+ 'EN' => 'Cool App'
+ ],$driver->getAppNames());
+ $driver->initialize();
}
/**
* @test
- * @depends testSetTitle01
+ * @depends testAppNames00
*/
- public function testSetTitle02() {
+ public function testAppNames01() {
$driver = new JsonDriver();
+ $this->assertEquals([
+ 'AR' => 'تطبيق',
+ 'EN' => 'Application'
+ ],$driver->getAppNames());
$driver->initialize();
$this->assertEquals([
- 'AR' => 'اوكي',
- 'EN' => 'Ok',
- 'JP' => 'Jap'
- ], $driver->getTitles());
- $this->assertEquals('Ok', $driver->getTitle('En'));
- $this->assertEquals('اوكي', $driver->getTitle('aR'));
- $this->assertEquals('Jap', $driver->getTitle('jp'));
- }
- /**
- * @test
- */
- public function testSetVersion00() {
- $driver = new JsonDriver();
- $this->assertEquals('1.0', $driver->getAppVersion());
- $this->assertEquals(date('Y-m-d'), $driver->getAppReleaseDate());
- $this->assertEquals('Stable', $driver->getAppVersionType());
- $driver->setAppVersion('2.0.0', 'Alpha', '2023-09-15');
+ 'AR' => 'تطبيق',
+ 'EN' => 'Cool App'
+ ],$driver->getAppNames());
+ $this->assertTrue(File::isFileExist(JsonDriver::getConfigPath().DS.$driver->getConfigFileName().'.json'));
+ $driver->remove();
+ $this->assertFalse(File::isFileExist(JsonDriver::getConfigPath().DS.$driver->getConfigFileName().'.json'));
}
/**
* @test
- * @depends testSetVersion00
*/
- public function testSetVersion01() {
+ public function testAppWithError00() {
+ $this->expectExceptionMessage('The property "username" of the connection "New_Connection" is missing.');
+ JsonDriver::setConfigFileName('config-with-err-00');
$driver = new JsonDriver();
$driver->initialize();
- $this->assertEquals('2.0.0', $driver->getAppVersion());
- $this->assertEquals('2023-09-15', $driver->getAppReleaseDate());
- $this->assertEquals('Alpha', $driver->getAppVersionType());
+ $driver->getDBConnections();
+ JsonDriver::setConfigFileName('app-config');
}
/**
* @test
*/
- public function testSetDescription00() {
+ public function testBase00() {
$driver = new JsonDriver();
- $this->assertEquals([
- 'AR' => '',
- 'EN' => ''
- ], $driver->getDescriptions());
- $this->assertEquals('', $driver->getDescription('En'));
- $this->assertEquals('', $driver->getDescription('aR'));
- $this->assertEquals('', $driver->getDescription('aRn'));
- $this->assertEquals('', $driver->getDescription(''));
- $driver->setDescription('Ok', 'En');
- $driver->setDescription('اوكي', 'ar');
+ $driver->setConfigFileName('app-config.json');
+ $driver->initialize(true);
+ $this->assertEquals('https://127.0.0.1', $driver->getBaseURL());
+ $driver->setBaseURL('https://home.com');
+ $this->assertEquals('https://home.com', $driver->getBaseURL());
+ $driver->setBaseURL('');
+ $this->assertEquals('https://127.0.0.1', $driver->getBaseURL());
}
-
/**
* @test
- * @depends testSetDescription00
*/
- public function testSetDescription01() {
+ public function testDatabaseConnections00() {
$driver = new JsonDriver();
- $driver->initialize();
- $this->assertEquals([
- 'AR' => 'اوكي',
- 'EN' => 'Ok'
- ], $driver->getDescriptions());
- $this->assertEquals('Ok', $driver->getDescription('En'));
- $this->assertEquals('اوكي', $driver->getDescription('aR'));
- $driver->setDescription('Jap', 'Jp');
- $driver->setDescription('Look', 'RUP');
- $driver->setDescription('', 'En');
+ $this->assertEquals(0, count($driver->getDBConnections()));
+ $this->assertNull($driver->getDBConnection('olf'));
+ $conn = new ConnectionInfo('mysql', 'root', 'test@222', 'my_db', 'localhost', 3306);
+ $driver->addOrUpdateDBConnection($conn);
+ $this->assertEquals(1, count($driver->getDBConnections()));
}
/**
* @test
- * @depends testSetDescription01
+ * @depends testDatabaseConnections00
*/
- public function testSetDescription02() {
+ public function testDatabaseConnections01() {
$driver = new JsonDriver();
$driver->initialize();
- $this->assertEquals([
- 'AR' => 'اوكي',
- 'EN' => '',
- 'JP' => 'Jap'
- ], $driver->getDescriptions());
- $this->assertEquals('', $driver->getDescription('En'));
- $this->assertEquals('اوكي', $driver->getDescription('aR'));
- $this->assertEquals('Jap', $driver->getDescription('jp'));
+ $account = $driver->getDBConnection('New_Connection');
+ $this->assertEquals(3306, $account->getPort());
+ $this->assertEquals('my_db', $account->getDBName());
+ $this->assertEquals('mysql', $account->getDatabaseType());
+ $this->assertEquals('localhost', $account->getHost());
+ $this->assertEquals('test@222', $account->getPassword());
+ $this->assertEquals('root', $account->getUsername());
+ $driver->removeAllDBConnections();
+ $this->assertEquals(0, count($driver->getDBConnections()));
+ $this->assertNull($driver->getDBConnection('New_Connection'));
}
/**
* @test
+ * @depends testDatabaseConnections01
*/
- public function testSetTitleSeparator00() {
+ public function testDatabaseConnections02() {
$driver = new JsonDriver();
- $this->assertEquals('|', $driver->getTitleSeparator());
- $driver->setTitleSeparator('*');
+ $this->assertEquals(0, count($driver->getDBConnections()));
+ $this->assertNull($driver->getDBConnection('olf'));
+ $conn = new ConnectionInfo('mysql', 'root', 'test@222', 'my_db', 'localhost', 3306, [
+ 'KG' => 9,
+ 'OP' => 'hello'
+ ]);
+ $driver->addOrUpdateDBConnection($conn);
+ $this->assertEquals(1, count($driver->getDBConnections()));
+ $account = $driver->getDBConnection('New_Connection');
+ $this->assertEquals(3306, $account->getPort());
+ $this->assertEquals('my_db', $account->getDBName());
+ $this->assertEquals('mysql', $account->getDatabaseType());
+ $this->assertEquals('localhost', $account->getHost());
+ $this->assertEquals('test@222', $account->getPassword());
+ $this->assertEquals('root', $account->getUsername());
+ $this->assertEquals([
+ 'KG' => 9,
+ 'OP' => 'hello'
+ ], $account->getExtars());
+ $driver->removeAllDBConnections();
+ $this->assertEquals(0, count($driver->getDBConnections()));
}
/**
* @test
- * @depends testSetTitleSeparator00
*/
- public function testSetTitleSeparator01() {
+ public function testDatabaseConnections03() {
$driver = new JsonDriver();
- $driver->initialize();
- $this->assertEquals('*', $driver->getTitleSeparator());
- $driver->setTitleSeparator('');
- }
- /**
+ $this->assertEquals(0, count($driver->getDBConnections()));
+ $this->assertNull($driver->getDBConnection('olf'));
+ $conn = new ConnectionInfo('mysql', 'root', 'test@222', 'my_db', 'localhost', 3306);
+ $conn->setName('ok');
+ $driver->addOrUpdateDBConnection($conn);
+ $this->assertEquals(1, count($driver->getDBConnections()));
+ $conn = new ConnectionInfo('mysql', 'root', 'test@222', 'my_db', 'localhost', 3306);
+ $conn->setName('not_ok');
+ $conn->setExtras([
+ 'A' => 'B',
+ 'C' => 'D'
+ ]);
+ $driver->addOrUpdateDBConnection($conn);
+ $this->assertEquals(2, count($driver->getDBConnections()));
+ $driver->removeDBConnection('ok');
+ $this->assertEquals(1, count($driver->getDBConnections()));
+
+ $account = $driver->getDBConnection('not_ok');
+ $this->assertEquals(3306, $account->getPort());
+ $this->assertEquals('my_db', $account->getDBName());
+ $this->assertEquals('mysql', $account->getDatabaseType());
+ $this->assertEquals('localhost', $account->getHost());
+ $this->assertEquals('test@222', $account->getPassword());
+ $this->assertEquals('root', $account->getUsername());
+ $this->assertEquals([
+ 'A' => 'B',
+ 'C' => 'D'
+ ], $account->getExtars());
+ }
+ /**
+ * @test
+ */
+ public function testHomePage00() {
+ $driver = new JsonDriver();
+ $driver->setConfigFileName('app-config.json');
+ $driver->initialize(true);
+ $this->assertEquals('https://127.0.0.1', $driver->getHomePage());
+ $driver->setHomePage('https://home.com/my-page');
+ $this->assertEquals('https://home.com/my-page', $driver->getHomePage());
+ $driver->setHomePage('');
+ $this->assertEquals('https://127.0.0.1', $driver->getHomePage());
+ }
+ /**
+ * @test
+ */
+ public function testSchedulerPass00() {
+ $driver = new JsonDriver();
+ $driver->setConfigFileName('app-config.json');
+ $driver->initialize(true);
+ $this->assertEquals('NO_PASSWORD', $driver->getSchedulerPassword());
+ $driver->setSchedulerPassword(hash('sha256', '123'));
+ $this->assertEquals(hash('sha256', '123'), $driver->getSchedulerPassword());
+ $driver->setSchedulerPassword('');
+ $this->assertEquals('NO_PASSWORD', $driver->getSchedulerPassword());
+ }
+ /**
+ * @test
+ */
+ public function testSetConfigFileName00() {
+ JsonDriver::setConfigFileName('app-config.json');
+ $this->assertEquals('app-config', JsonDriver::getConfigFileName());
+ JsonDriver::setConfigFileName('super-conf.json');
+ $this->assertEquals('super-conf', JsonDriver::getConfigFileName());
+ JsonDriver::setConfigFileName('super-confx.kkp');
+ $this->assertEquals('super-confx', JsonDriver::getConfigFileName());
+ }
+ /**
+ * @test
+ */
+ public function testSetDescription00() {
+ $driver = new JsonDriver();
+ $this->assertEquals([
+ 'AR' => '',
+ 'EN' => ''
+ ], $driver->getDescriptions());
+ $this->assertEquals('', $driver->getDescription('En'));
+ $this->assertEquals('', $driver->getDescription('aR'));
+ $this->assertEquals('', $driver->getDescription('aRn'));
+ $this->assertEquals('', $driver->getDescription(''));
+ $driver->setDescription('Ok', 'En');
+ $driver->setDescription('اوكي', 'ar');
+ }
+
+ /**
+ * @test
+ * @depends testSetDescription00
+ */
+ public function testSetDescription01() {
+ $driver = new JsonDriver();
+ $driver->initialize();
+ $this->assertEquals([
+ 'AR' => 'اوكي',
+ 'EN' => 'Ok'
+ ], $driver->getDescriptions());
+ $this->assertEquals('Ok', $driver->getDescription('En'));
+ $this->assertEquals('اوكي', $driver->getDescription('aR'));
+ $driver->setDescription('Jap', 'Jp');
+ $driver->setDescription('Look', 'RUP');
+ $driver->setDescription('', 'En');
+ }
+ /**
+ * @test
+ * @depends testSetDescription01
+ */
+ public function testSetDescription02() {
+ $driver = new JsonDriver();
+ $driver->initialize();
+ $this->assertEquals([
+ 'AR' => 'اوكي',
+ 'EN' => '',
+ 'JP' => 'Jap'
+ ], $driver->getDescriptions());
+ $this->assertEquals('', $driver->getDescription('En'));
+ $this->assertEquals('اوكي', $driver->getDescription('aR'));
+ $this->assertEquals('Jap', $driver->getDescription('jp'));
+ }
+ /**
+ * @test
+ */
+ public function testSetPrimaryLanguage00() {
+ $driver = new JsonDriver();
+ $this->assertEquals('EN', $driver->getPrimaryLanguage());
+ $driver->setPrimaryLanguage('ar');
+ }
+ /**
+ * @test
+ * @depends testSetPrimaryLanguage00
+ */
+ public function testSetPrimaryLanguage01() {
+ $driver = new JsonDriver();
+ $driver->initialize();
+ $this->assertEquals('AR', $driver->getPrimaryLanguage());
+ $driver->setPrimaryLanguage('');
+ }
+ /**
+ * @test
+ * @depends testSetPrimaryLanguage01
+ */
+ public function testSetPrimaryLanguage02() {
+ $driver = new JsonDriver();
+ $driver->initialize();
+ $this->assertEquals('AR', $driver->getPrimaryLanguage());
+ }
+ /**
+ * @test
+ */
+ public function testSetTheme00() {
+ $driver = new JsonDriver();
+ $driver->setConfigFileName('app-config.json');
+ $driver->initialize(true);
+ $this->assertEquals('', $driver->getTheme());
+ $driver->setTheme('Test Theme');
+ $this->assertEquals('Test Theme', $driver->getTheme());
+ $driver->setTheme('');
+ $this->assertEquals('', $driver->getTheme());
+ }
+ /**
+ * @test
+ */
+ public function testSetTitle00() {
+ $driver = new JsonDriver();
+ $this->assertEquals([
+ 'AR' => 'افتراضي',
+ 'EN' => 'Default'
+ ], $driver->getTitles());
+ $this->assertEquals('Default', $driver->getTitle('En'));
+ $this->assertEquals('افتراضي', $driver->getTitle('aR'));
+ $this->assertEquals('', $driver->getTitle('aRn'));
+ $this->assertEquals('', $driver->getTitle(''));
+ $driver->setTitle('Ok', 'En');
+ $driver->setTitle('اوكي', 'ar');
+ }
+ /**
+ * @test
+ * @depends testSetTitle00
+ */
+ public function testSetTitle01() {
+ $driver = new JsonDriver();
+ $driver->initialize();
+ $this->assertEquals([
+ 'AR' => 'اوكي',
+ 'EN' => 'Ok'
+ ], $driver->getTitles());
+ $this->assertEquals('Ok', $driver->getTitle('En'));
+ $this->assertEquals('اوكي', $driver->getTitle('aR'));
+ $driver->setTitle('Jap', 'Jp');
+ $driver->setTitle('Look', 'RUP');
+ $driver->setTitle('', 'En');
+ }
+ /**
+ * @test
+ * @depends testSetTitle01
+ */
+ public function testSetTitle02() {
+ $driver = new JsonDriver();
+ $driver->initialize();
+ $this->assertEquals([
+ 'AR' => 'اوكي',
+ 'EN' => 'Ok',
+ 'JP' => 'Jap'
+ ], $driver->getTitles());
+ $this->assertEquals('Ok', $driver->getTitle('En'));
+ $this->assertEquals('اوكي', $driver->getTitle('aR'));
+ $this->assertEquals('Jap', $driver->getTitle('jp'));
+ }
+ /**
+ * @test
+ */
+ public function testSetTitleSeparator00() {
+ $driver = new JsonDriver();
+ $this->assertEquals('|', $driver->getTitleSeparator());
+ $driver->setTitleSeparator('*');
+ }
+ /**
+ * @test
+ * @depends testSetTitleSeparator00
+ */
+ public function testSetTitleSeparator01() {
+ $driver = new JsonDriver();
+ $driver->initialize();
+ $this->assertEquals('*', $driver->getTitleSeparator());
+ $driver->setTitleSeparator('');
+ }
+ /**
* @test
* @depends testSetTitleSeparator01
*/
@@ -368,6 +501,27 @@ public function testSetTitleSeparator02() {
$driver->initialize();
$this->assertEquals('*', $driver->getTitleSeparator());
}
+ /**
+ * @test
+ */
+ public function testSetVersion00() {
+ $driver = new JsonDriver();
+ $this->assertEquals('1.0', $driver->getAppVersion());
+ $this->assertEquals(date('Y-m-d'), $driver->getAppReleaseDate());
+ $this->assertEquals('Stable', $driver->getAppVersionType());
+ $driver->setAppVersion('2.0.0', 'Alpha', '2023-09-15');
+ }
+ /**
+ * @test
+ * @depends testSetVersion00
+ */
+ public function testSetVersion01() {
+ $driver = new JsonDriver();
+ $driver->initialize();
+ $this->assertEquals('2.0.0', $driver->getAppVersion());
+ $this->assertEquals('2023-09-15', $driver->getAppReleaseDate());
+ $this->assertEquals('Alpha', $driver->getAppVersionType());
+ }
/**
* @test
*/
@@ -412,7 +566,7 @@ public function testSMTPConnections01() {
public function testSMTPConnections02() {
$driver = new JsonDriver();
$driver->initialize();
- $account =$driver->getSMTPConnection('Cool');
+ $account = $driver->getSMTPConnection('Cool');
$this->assertEquals(990, $account->getPort());
$this->assertEquals('Cool', $account->getAccountName());
$this->assertEquals('addr@example.com', $account->getAddress());
@@ -460,7 +614,7 @@ public function testSMTPConnections04() {
]);
$driver->addOrUpdateSMTPAccount($conn);
$this->assertEquals(2, count($driver->getSMTPConnections()));
- $account =$driver->getSMTPConnection('Cool');
+ $account = $driver->getSMTPConnection('Cool');
$this->assertEquals(6, $account->getPort());
$this->assertEquals('Cool', $account->getAccountName());
$this->assertEquals('addr@example.com', $account->getAddress());
@@ -474,159 +628,4 @@ public function testSMTPConnections04() {
$account = $driver->getSMTPConnection('Cool2');
$this->assertNotNull($account);
}
- /**
- * @test
- */
- public function testDatabaseConnections00() {
- $driver = new JsonDriver();
- $this->assertEquals(0, count($driver->getDBConnections()));
- $this->assertNull($driver->getDBConnection('olf'));
- $conn = new ConnectionInfo('mysql', 'root', 'test@222', 'my_db', 'localhost', 3306);
- $driver->addOrUpdateDBConnection($conn);
- $this->assertEquals(1, count($driver->getDBConnections()));
-
- }
- /**
- * @test
- * @depends testDatabaseConnections00
- */
- public function testDatabaseConnections01() {
- $driver = new JsonDriver();
- $driver->initialize();
- $account = $driver->getDBConnection('New_Connection');
- $this->assertEquals(3306, $account->getPort());
- $this->assertEquals('my_db', $account->getDBName());
- $this->assertEquals('mysql', $account->getDatabaseType());
- $this->assertEquals('localhost', $account->getHost());
- $this->assertEquals('test@222', $account->getPassword());
- $this->assertEquals('root', $account->getUsername());
- $driver->removeAllDBConnections();
- $this->assertEquals(0, count($driver->getDBConnections()));
- $this->assertNull($driver->getDBConnection('New_Connection'));
- }
- /**
- * @test
- * @depends testDatabaseConnections01
- */
- public function testDatabaseConnections02() {
- $driver = new JsonDriver();
- $this->assertEquals(0, count($driver->getDBConnections()));
- $this->assertNull($driver->getDBConnection('olf'));
- $conn = new ConnectionInfo('mysql', 'root', 'test@222', 'my_db', 'localhost', 3306, [
- 'KG' => 9,
- 'OP' => 'hello'
- ]);
- $driver->addOrUpdateDBConnection($conn);
- $this->assertEquals(1, count($driver->getDBConnections()));
- $account = $driver->getDBConnection('New_Connection');
- $this->assertEquals(3306, $account->getPort());
- $this->assertEquals('my_db', $account->getDBName());
- $this->assertEquals('mysql', $account->getDatabaseType());
- $this->assertEquals('localhost', $account->getHost());
- $this->assertEquals('test@222', $account->getPassword());
- $this->assertEquals('root', $account->getUsername());
- $this->assertEquals([
- 'KG' => 9,
- 'OP' => 'hello'
- ], $account->getExtars());
- $driver->removeAllDBConnections();
- $this->assertEquals(0, count($driver->getDBConnections()));
- }
- /**
- * @test
- */
- public function testDatabaseConnections03() {
- $driver = new JsonDriver();
- $this->assertEquals(0, count($driver->getDBConnections()));
- $this->assertNull($driver->getDBConnection('olf'));
- $conn = new ConnectionInfo('mysql', 'root', 'test@222', 'my_db', 'localhost', 3306);
- $conn->setName('ok');
- $driver->addOrUpdateDBConnection($conn);
- $this->assertEquals(1, count($driver->getDBConnections()));
- $conn = new ConnectionInfo('mysql', 'root', 'test@222', 'my_db', 'localhost', 3306);
- $conn->setName('not_ok');
- $conn->setExtras([
- 'A' => 'B',
- 'C' => 'D'
- ]);
- $driver->addOrUpdateDBConnection($conn);
- $this->assertEquals(2, count($driver->getDBConnections()));
- $driver->removeDBConnection('ok');
- $this->assertEquals(1, count($driver->getDBConnections()));
-
- $account = $driver->getDBConnection('not_ok');
- $this->assertEquals(3306, $account->getPort());
- $this->assertEquals('my_db', $account->getDBName());
- $this->assertEquals('mysql', $account->getDatabaseType());
- $this->assertEquals('localhost', $account->getHost());
- $this->assertEquals('test@222', $account->getPassword());
- $this->assertEquals('root', $account->getUsername());
- $this->assertEquals([
- 'A' => 'B',
- 'C' => 'D'
- ], $account->getExtars());
- }
- /**
- * @test
- */
- public function testAppWithError00() {
- $this->expectExceptionMessage('The property "username" of the connection "New_Connection" is missing.');
- JsonDriver::setConfigFileName('config-with-err-00');
- $driver = new JsonDriver();
- $driver->initialize();
- $driver->getDBConnections();
- JsonDriver::setConfigFileName('app-config');
- }
- /**
- * @test
- */
- public function testSchedulerPass00() {
- $driver = new JsonDriver();
- $driver->setConfigFileName('app-config.json');
- $driver->initialize(true);
- $this->assertEquals('NO_PASSWORD', $driver->getSchedulerPassword());
- $driver->setSchedulerPassword(hash('sha256', '123'));
- $this->assertEquals(hash('sha256', '123'), $driver->getSchedulerPassword());
- $driver->setSchedulerPassword('');
- $this->assertEquals('NO_PASSWORD', $driver->getSchedulerPassword());
- }
- /**
- * @test
- */
- public function testHomePage00() {
- $driver = new JsonDriver();
- $driver->setConfigFileName('app-config.json');
- $driver->initialize(true);
- $this->assertEquals('https://127.0.0.1', $driver->getHomePage());
- $driver->setHomePage('https://home.com/my-page');
- $this->assertEquals('https://home.com/my-page', $driver->getHomePage());
- $driver->setHomePage('');
- $this->assertEquals('https://127.0.0.1', $driver->getHomePage());
- }
- /**
- * @test
- */
- public function testBase00() {
- $driver = new JsonDriver();
- $driver->setConfigFileName('app-config.json');
- $driver->initialize(true);
- $this->assertEquals('https://127.0.0.1', $driver->getBaseURL());
- $driver->setBaseURL('https://home.com');
- $this->assertEquals('https://home.com', $driver->getBaseURL());
- $driver->setBaseURL('');
- $this->assertEquals('https://127.0.0.1', $driver->getBaseURL());
- }
- /**
- * @test
- */
- public function testSetTheme00() {
- $driver = new JsonDriver();
- $driver->setConfigFileName('app-config.json');
- $driver->initialize(true);
- $this->assertEquals('', $driver->getTheme());
- $driver->setTheme('Test Theme');
- $this->assertEquals('Test Theme', $driver->getTheme());
- $driver->setTheme('');
- $this->assertEquals('', $driver->getTheme());
- }
}
From 3afeaec247751d780771a0ee5e2ced586b5a180d Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Sun, 8 Feb 2026 23:32:55 +0300
Subject: [PATCH 57/88] chore: Rename Folders
---
App/Database/empl/temp | 1 -
App/Database/migrations/.gitkeep | 1 -
App/Database/migrations/commands/.gitkeep | 1 -
.../migrations/emptyRunner/XRunner.php | 13 --------
.../migrations/multi/Migration000.php | 32 -------------------
.../migrations/multi/Migration001.php | 32 -------------------
.../migrations/multi/Migration002.php | 32 -------------------
App/Database/migrations/multi/MultiRunner.php | 18 -----------
.../migrations/multiDownErr/Migration000.php | 30 -----------------
.../migrations/multiDownErr/Migration001.php | 32 -------------------
.../migrations/multiDownErr/Migration002.php | 32 -------------------
.../multiDownErr/MultiErrRunner.php | 15 ---------
.../migrations/multiErr/Migration000.php | 32 -------------------
.../migrations/multiErr/Migration001.php | 32 -------------------
.../migrations/multiErr/Migration002.php | 32 -------------------
.../migrations/multiErr/MultiErrRunner.php | 15 ---------
.../migrations/noConn/Migration000.php | 32 -------------------
App/Database/migrations/noConn/XRunner.php | 11 -------
App/Database/super/temp | 1 -
19 files changed, 394 deletions(-)
delete mode 100644 App/Database/empl/temp
delete mode 100644 App/Database/migrations/.gitkeep
delete mode 100644 App/Database/migrations/commands/.gitkeep
delete mode 100644 App/Database/migrations/emptyRunner/XRunner.php
delete mode 100644 App/Database/migrations/multi/Migration000.php
delete mode 100644 App/Database/migrations/multi/Migration001.php
delete mode 100644 App/Database/migrations/multi/Migration002.php
delete mode 100644 App/Database/migrations/multi/MultiRunner.php
delete mode 100644 App/Database/migrations/multiDownErr/Migration000.php
delete mode 100644 App/Database/migrations/multiDownErr/Migration001.php
delete mode 100644 App/Database/migrations/multiDownErr/Migration002.php
delete mode 100644 App/Database/migrations/multiDownErr/MultiErrRunner.php
delete mode 100644 App/Database/migrations/multiErr/Migration000.php
delete mode 100644 App/Database/migrations/multiErr/Migration001.php
delete mode 100644 App/Database/migrations/multiErr/Migration002.php
delete mode 100644 App/Database/migrations/multiErr/MultiErrRunner.php
delete mode 100644 App/Database/migrations/noConn/Migration000.php
delete mode 100644 App/Database/migrations/noConn/XRunner.php
delete mode 100644 App/Database/super/temp
diff --git a/App/Database/empl/temp b/App/Database/empl/temp
deleted file mode 100644
index 8b1378917..000000000
--- a/App/Database/empl/temp
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/App/Database/migrations/.gitkeep b/App/Database/migrations/.gitkeep
deleted file mode 100644
index 8b1378917..000000000
--- a/App/Database/migrations/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/App/Database/migrations/commands/.gitkeep b/App/Database/migrations/commands/.gitkeep
deleted file mode 100644
index 8b1378917..000000000
--- a/App/Database/migrations/commands/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/App/Database/migrations/emptyRunner/XRunner.php b/App/Database/migrations/emptyRunner/XRunner.php
deleted file mode 100644
index da684d498..000000000
--- a/App/Database/migrations/emptyRunner/XRunner.php
+++ /dev/null
@@ -1,13 +0,0 @@
-getDBConnection('default-conn');
- parent::__construct(null);
- }
-}
diff --git a/App/Database/migrations/multi/Migration000.php b/App/Database/migrations/multi/Migration000.php
deleted file mode 100644
index 72155beef..000000000
--- a/App/Database/migrations/multi/Migration000.php
+++ /dev/null
@@ -1,32 +0,0 @@
- 'true'
- ]);
- parent::__construct($conn);
- $this->register(Migration000::class);
- $this->register(Migration001::class);
- $this->register(Migration002::class);
- }
-}
diff --git a/App/Database/migrations/multiDownErr/Migration000.php b/App/Database/migrations/multiDownErr/Migration000.php
deleted file mode 100644
index 6c905b73a..000000000
--- a/App/Database/migrations/multiDownErr/Migration000.php
+++ /dev/null
@@ -1,30 +0,0 @@
-do();
- }
- /**
- * Performs the action that will apply the migration.
- *
- * @param Database $schema The database at which the migration will be applied to.
- */
- public function up(Database $schema) {
- //TODO: Implement the action which will apply the migration to database.
- }
-}
diff --git a/App/Database/migrations/multiDownErr/Migration002.php b/App/Database/migrations/multiDownErr/Migration002.php
deleted file mode 100644
index 94bcb98c1..000000000
--- a/App/Database/migrations/multiDownErr/Migration002.php
+++ /dev/null
@@ -1,32 +0,0 @@
- 'true'
- ]);
- parent::__construct($conn);
- }
-}
diff --git a/App/Database/migrations/multiErr/Migration000.php b/App/Database/migrations/multiErr/Migration000.php
deleted file mode 100644
index 6e024ac4e..000000000
--- a/App/Database/migrations/multiErr/Migration000.php
+++ /dev/null
@@ -1,32 +0,0 @@
-y();
- }
- /**
- * Performs the action that will apply the migration.
- *
- * @param Database $schema The database at which the migration will be applied to.
- */
- public function up(Database $schema) {
- $this->x();
- }
-}
diff --git a/App/Database/migrations/multiErr/Migration001.php b/App/Database/migrations/multiErr/Migration001.php
deleted file mode 100644
index 2ca424b04..000000000
--- a/App/Database/migrations/multiErr/Migration001.php
+++ /dev/null
@@ -1,32 +0,0 @@
- 'true'
- ]);
- parent::__construct($conn);
- }
-}
diff --git a/App/Database/migrations/noConn/Migration000.php b/App/Database/migrations/noConn/Migration000.php
deleted file mode 100644
index 49ffc83bc..000000000
--- a/App/Database/migrations/noConn/Migration000.php
+++ /dev/null
@@ -1,32 +0,0 @@
-y();
- }
- /**
- * Performs the action that will apply the migration.
- *
- * @param Database $schema The database at which the migration will be applied to.
- */
- public function up(Database $schema) {
- $this->x();
- }
-}
diff --git a/App/Database/migrations/noConn/XRunner.php b/App/Database/migrations/noConn/XRunner.php
deleted file mode 100644
index ffbaa574f..000000000
--- a/App/Database/migrations/noConn/XRunner.php
+++ /dev/null
@@ -1,11 +0,0 @@
-
Date: Sun, 8 Feb 2026 23:39:57 +0300
Subject: [PATCH 58/88] chore: Rename Folders
---
App/Database/Empl/temp | 1 +
App/Database/Migrations/.gitkeep | 1 +
App/Database/Migrations/Commands/.gitkeep | 1 +
.../Migrations/EmptyRunner/XRunner.php | 13 ++++++++
.../Migrations/Multi/Migration000.php | 32 +++++++++++++++++++
.../Migrations/Multi/Migration001.php | 32 +++++++++++++++++++
.../Migrations/Multi/Migration002.php | 32 +++++++++++++++++++
App/Database/Migrations/Multi/MultiRunner.php | 18 +++++++++++
.../Migrations/MultiDownErr/Migration000.php | 30 +++++++++++++++++
.../Migrations/MultiDownErr/Migration001.php | 32 +++++++++++++++++++
.../Migrations/MultiDownErr/Migration002.php | 32 +++++++++++++++++++
.../MultiDownErr/MultiErrRunner.php | 15 +++++++++
.../Migrations/MultiErr/Migration000.php | 32 +++++++++++++++++++
.../Migrations/MultiErr/Migration001.php | 32 +++++++++++++++++++
.../Migrations/MultiErr/Migration002.php | 32 +++++++++++++++++++
.../Migrations/MultiErr/MultiErrRunner.php | 15 +++++++++
.../Migrations/NoConn/Migration000.php | 32 +++++++++++++++++++
App/Database/Migrations/NoConn/XRunner.php | 11 +++++++
App/Database/Super/temp | 1 +
19 files changed, 394 insertions(+)
create mode 100644 App/Database/Empl/temp
create mode 100644 App/Database/Migrations/.gitkeep
create mode 100644 App/Database/Migrations/Commands/.gitkeep
create mode 100644 App/Database/Migrations/EmptyRunner/XRunner.php
create mode 100644 App/Database/Migrations/Multi/Migration000.php
create mode 100644 App/Database/Migrations/Multi/Migration001.php
create mode 100644 App/Database/Migrations/Multi/Migration002.php
create mode 100644 App/Database/Migrations/Multi/MultiRunner.php
create mode 100644 App/Database/Migrations/MultiDownErr/Migration000.php
create mode 100644 App/Database/Migrations/MultiDownErr/Migration001.php
create mode 100644 App/Database/Migrations/MultiDownErr/Migration002.php
create mode 100644 App/Database/Migrations/MultiDownErr/MultiErrRunner.php
create mode 100644 App/Database/Migrations/MultiErr/Migration000.php
create mode 100644 App/Database/Migrations/MultiErr/Migration001.php
create mode 100644 App/Database/Migrations/MultiErr/Migration002.php
create mode 100644 App/Database/Migrations/MultiErr/MultiErrRunner.php
create mode 100644 App/Database/Migrations/NoConn/Migration000.php
create mode 100644 App/Database/Migrations/NoConn/XRunner.php
create mode 100644 App/Database/Super/temp
diff --git a/App/Database/Empl/temp b/App/Database/Empl/temp
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/App/Database/Empl/temp
@@ -0,0 +1 @@
+
diff --git a/App/Database/Migrations/.gitkeep b/App/Database/Migrations/.gitkeep
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/App/Database/Migrations/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/App/Database/Migrations/Commands/.gitkeep b/App/Database/Migrations/Commands/.gitkeep
new file mode 100644
index 000000000..8b1378917
--- /dev/null
+++ b/App/Database/Migrations/Commands/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/App/Database/Migrations/EmptyRunner/XRunner.php b/App/Database/Migrations/EmptyRunner/XRunner.php
new file mode 100644
index 000000000..da684d498
--- /dev/null
+++ b/App/Database/Migrations/EmptyRunner/XRunner.php
@@ -0,0 +1,13 @@
+getDBConnection('default-conn');
+ parent::__construct(null);
+ }
+}
diff --git a/App/Database/Migrations/Multi/Migration000.php b/App/Database/Migrations/Multi/Migration000.php
new file mode 100644
index 000000000..72155beef
--- /dev/null
+++ b/App/Database/Migrations/Multi/Migration000.php
@@ -0,0 +1,32 @@
+ 'true'
+ ]);
+ parent::__construct($conn);
+ $this->register(Migration000::class);
+ $this->register(Migration001::class);
+ $this->register(Migration002::class);
+ }
+}
diff --git a/App/Database/Migrations/MultiDownErr/Migration000.php b/App/Database/Migrations/MultiDownErr/Migration000.php
new file mode 100644
index 000000000..6c905b73a
--- /dev/null
+++ b/App/Database/Migrations/MultiDownErr/Migration000.php
@@ -0,0 +1,30 @@
+do();
+ }
+ /**
+ * Performs the action that will apply the migration.
+ *
+ * @param Database $schema The database at which the migration will be applied to.
+ */
+ public function up(Database $schema) {
+ //TODO: Implement the action which will apply the migration to database.
+ }
+}
diff --git a/App/Database/Migrations/MultiDownErr/Migration002.php b/App/Database/Migrations/MultiDownErr/Migration002.php
new file mode 100644
index 000000000..94bcb98c1
--- /dev/null
+++ b/App/Database/Migrations/MultiDownErr/Migration002.php
@@ -0,0 +1,32 @@
+ 'true'
+ ]);
+ parent::__construct($conn);
+ }
+}
diff --git a/App/Database/Migrations/MultiErr/Migration000.php b/App/Database/Migrations/MultiErr/Migration000.php
new file mode 100644
index 000000000..6e024ac4e
--- /dev/null
+++ b/App/Database/Migrations/MultiErr/Migration000.php
@@ -0,0 +1,32 @@
+y();
+ }
+ /**
+ * Performs the action that will apply the migration.
+ *
+ * @param Database $schema The database at which the migration will be applied to.
+ */
+ public function up(Database $schema) {
+ $this->x();
+ }
+}
diff --git a/App/Database/Migrations/MultiErr/Migration001.php b/App/Database/Migrations/MultiErr/Migration001.php
new file mode 100644
index 000000000..2ca424b04
--- /dev/null
+++ b/App/Database/Migrations/MultiErr/Migration001.php
@@ -0,0 +1,32 @@
+ 'true'
+ ]);
+ parent::__construct($conn);
+ }
+}
diff --git a/App/Database/Migrations/NoConn/Migration000.php b/App/Database/Migrations/NoConn/Migration000.php
new file mode 100644
index 000000000..49ffc83bc
--- /dev/null
+++ b/App/Database/Migrations/NoConn/Migration000.php
@@ -0,0 +1,32 @@
+y();
+ }
+ /**
+ * Performs the action that will apply the migration.
+ *
+ * @param Database $schema The database at which the migration will be applied to.
+ */
+ public function up(Database $schema) {
+ $this->x();
+ }
+}
diff --git a/App/Database/Migrations/NoConn/XRunner.php b/App/Database/Migrations/NoConn/XRunner.php
new file mode 100644
index 000000000..ffbaa574f
--- /dev/null
+++ b/App/Database/Migrations/NoConn/XRunner.php
@@ -0,0 +1,11 @@
+
Date: Mon, 9 Feb 2026 00:18:51 +0300
Subject: [PATCH 59/88] test: Fix Test Case
---
.../Tests/Cli/CreateMiddlewareCommandTest.php | 20 +++++++++++++++++--
1 file changed, 18 insertions(+), 2 deletions(-)
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
index 2bf49bdd6..2d5d2c5b3 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
@@ -157,13 +157,29 @@ public function testCreateMiddlewareWithArgs01() {
* @test
*/
public function testCreateMiddlewareWithArgs02() {
+ $className = 'TestMd'.time();
$output = $this->executeMultiCommand([
CreateMiddlewareCommand::class,
'--class-name' => '',
+ ], [
+ $className,
+ "\n", // Use default middleware name (same as class name)
+ '50',
+ "\n"
]);
- $this->assertEquals(-1, $this->getExitCode());
- $this->assertContains("Error: Class name cannot be empty.\n", $output);
+ $this->assertEquals([
+ "Error: --class-name cannot be empty string.\n",
+ "Enter middleware class name:\n",
+ "Enter middleware name: Enter = '$className'\n",
+ "Enter middleware priority: Enter = '0'\n",
+ "Add middleware to groups?(y/N)\n",
+ "Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
+
+ $this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
}
/**
* @test
From f598610d039dd5b7f505e714de8c92bdb631d119 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 00:19:13 +0300
Subject: [PATCH 60/88] fix: Method Call
---
WebFiori/Framework/App.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 9367e843f..3684be878 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -374,7 +374,7 @@ public static function getRunner() : Runner {
$r->register(new $c());
}
$r->setDefaultCommand('help');
- self::call(APP_DIR.'\Ini\Commands::init');
+ self::call(APP_DIR.'\Ini\Commands::initialize');
});
}
From e5f845cdeea492ba59d0069da0c06114d8badc37 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 00:19:43 +0300
Subject: [PATCH 61/88] fix: Check for Empty `--class-name`
---
WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php b/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php
index 9d44eb6fd..1971740e0 100644
--- a/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php
+++ b/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php
@@ -89,7 +89,10 @@ private function getMDName(string $className) : string {
*/
public function exec() : int {
$className = $this->getArgValue('--class-name');
-
+ if ($className !== null && strlen($className) == 0) {
+ $this->error('--class-name cannot be empty string.');
+ $className = null;
+ }
if ($className === null) {
$validator = new InputValidator(function($input) {
return !empty(trim($input));
From e82be41325d184164bc9d5283ccb741808a24aed Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 00:24:10 +0300
Subject: [PATCH 62/88] test: Updated Test Cases
---
.../Tests/Cli/CreateMiddlewareCommandTest.php | 34 ++++++++++++-------
1 file changed, 22 insertions(+), 12 deletions(-)
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
index 2d5d2c5b3..0534ea7be 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php
@@ -52,16 +52,16 @@ public function testCreateMiddleware01() {
]);
$this->assertEquals(0, $this->getExitCode());
- $output = $this->getOutput();
-
- $this->assertEquals("Enter middleware class name:\n", $output[0]);
- $this->assertEquals("Enter middleware name: Enter = '$className'\n", $output[1]);
- $this->assertEquals("Enter middleware priority: Enter = '0'\n", $output[2]);
- $this->assertEquals("Add middleware to groups?(y/N)\n", $output[3]);
- $this->assertEquals("Enter group name (leave empty to finish):\n", $output[4]);
- $this->assertEquals("Enter group name (leave empty to finish):\n", $output[5]);
- $this->assertEquals("Enter group name (leave empty to finish):\n", $output[6]);
- $this->assertEquals("Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n", $output[7]);
+ $this->assertEquals([
+ "Enter middleware class name:\n",
+ "Enter middleware name: Enter = '$className'\n",
+ "Enter middleware priority: Enter = '0'\n",
+ "Add middleware to groups?(y/N)\n",
+ "Enter group name (leave empty to finish):\n",
+ "Enter group name (leave empty to finish):\n",
+ "Enter group name (leave empty to finish):\n",
+ "Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
+ ], $output);
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
@@ -129,8 +129,10 @@ public function testCreateMiddlewareWithArgs00() {
'--groups' => ''
]);
+ $this->assertEquals([
+ "Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
+ ], $output);
$this->assertEquals(0, $this->getExitCode());
- $this->assertContains("Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n", $output);
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
@@ -149,6 +151,9 @@ public function testCreateMiddlewareWithArgs01() {
'--groups' => 'api,web,admin'
]);
+ $this->assertEquals([
+ "Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
+ ], $output);
$this->assertEquals(0, $this->getExitCode());
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
@@ -195,8 +200,10 @@ public function testCreateMiddlewareWithArgs03() {
'--groups' => ''
]);
+ $this->assertEquals([
+ "Error: Priority must be a number.\n"
+ ], $output);
$this->assertEquals(-1, $this->getExitCode());
- $this->assertContains("Error: Priority must be a number.\n", $output);
}
/**
* @test
@@ -212,6 +219,9 @@ public function testCreateMiddlewareWithArgs04() {
'--groups' => 'api,web'
]);
+ $this->assertEquals([
+ "Success: Middleware class created at: ".APP_PATH."Middleware".DIRECTORY_SEPARATOR.$className."Middleware.php\n"
+ ], $output);
$this->assertEquals(0, $this->getExitCode());
$this->assertTrue(class_exists('\\App\\Middleware\\'.$className.'Middleware'));
$this->removeClass('\\App\\Middleware\\'.$className.'Middleware');
From 192c2f02ea2da20e28ed577c6211bc17f4052b4f Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 19:06:59 +0300
Subject: [PATCH 63/88] fix: Initialization Path
---
WebFiori/Framework/Ini.php | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/WebFiori/Framework/Ini.php b/WebFiori/Framework/Ini.php
index 80320b3e9..6996d87b7 100644
--- a/WebFiori/Framework/Ini.php
+++ b/WebFiori/Framework/Ini.php
@@ -51,8 +51,8 @@ private function __construct() {
public static function createAppDirs() {
$DS = DIRECTORY_SEPARATOR;
self::mkdir(ROOT_PATH.$DS.APP_DIR);
- self::mkdir(ROOT_PATH.$DS.APP_DIR.$DS.'Init');
- self::mkdir(ROOT_PATH.$DS.APP_DIR.$DS.'Init'.$DS.'Routes');
+ self::mkdir(ROOT_PATH.$DS.APP_DIR.$DS.'Ini');
+ self::mkdir(ROOT_PATH.$DS.APP_DIR.$DS.'Ini'.$DS.'Routes');
self::mkdir(ROOT_PATH.$DS.APP_DIR.$DS.'Pages');
self::mkdir(ROOT_PATH.$DS.APP_DIR.$DS.'Commands');
self::mkdir(ROOT_PATH.$DS.APP_DIR.$DS.'Tasks');
@@ -83,13 +83,13 @@ public static function createAppDirs() {
* @throws FileException
*/
public function createIniClass(string $className, string $comment) {
- $cFile = new File("$className.php", APP_PATH.'Init');
+ $cFile = new File("$className.php", APP_PATH.'Ini');
$cFile->remove();
$cFile->create();
ClassDriver::a($cFile, [
"docEmptyLine,
$this->docEnd,
- 'public static function init() {'
+ 'public static function initialize() {'
], 1);
ClassDriver::a($cFile, "", 3);
ClassDriver::a($cFile, "}", 1);
ClassDriver::a($cFile, "}");
$cFile->create(true);
$cFile->write();
- require_once APP_PATH.'Init'.DS."$className.php";
+ require_once APP_PATH.'Ini'.DS."$className.php";
}
/**
@@ -121,11 +121,11 @@ public function createIniClass(string $className, string $comment) {
* @throws FileException
*/
public function createRoutesClass(string $className) {
- $cFile = new File("$className.php", APP_PATH.'Init'.DS.'Routes');
+ $cFile = new File("$className.php", APP_PATH.'Ini'.DS.'Routes');
$cFile->remove();
ClassDriver::a($cFile, "
Date: Mon, 9 Feb 2026 23:20:36 +0300
Subject: [PATCH 64/88] refactor: Move `run` Migrations
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/RunMigrationsCommandNew.php | 118 ++++++++++++++++++
.../Tests/Cli/RunMigrationsCommandNewTest.php | 92 ++++++++++++++
3 files changed, 211 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandNewTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 3684be878..ef4d02cae 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -368,6 +368,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew',
];
foreach ($commands as $c) {
diff --git a/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php b/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
new file mode 100644
index 000000000..d1a7e72c0
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
@@ -0,0 +1,118 @@
+getConnection();
+ if ($connection === null) {
+ return 1;
+ }
+
+ $env = $this->getArgValue('--env') ?? 'dev';
+ $this->runner = new SchemaRunner($connection, $env);
+
+ // Discover migrations
+ $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
+ $namespace = APP_DIR.'\\Database\\Migrations';
+ $count = $this->runner->discoverFromPath($migrationsPath, $namespace);
+
+ if ($count === 0) {
+ $this->info('No migrations found.');
+ return 0;
+ }
+
+ return $this->runMigrations();
+
+ } catch (Throwable $e) {
+ $this->error('An exception was thrown.');
+ $this->println('Message: ' . $e->getMessage());
+ $this->println('File: ' . $e->getFile() . ':' . $e->getLine());
+ return 1;
+ }
+ }
+
+ private function getConnection(): ?ConnectionInfo {
+ $connections = App::getConfig()->getDBConnections();
+
+ if (empty($connections)) {
+ $this->info('No database connections configured.');
+ return null;
+ }
+
+ $connectionName = $this->getArgValue('--connection');
+
+ if ($connectionName !== null) {
+ $connection = App::getConfig()->getDBConnection($connectionName);
+ if ($connection === null) {
+ $this->error("Connection '$connectionName' not found.");
+ return null;
+ }
+ return $connection;
+ }
+
+ return CLIUtils::getConnectionName($this);
+ }
+
+ private function runMigrations(): int {
+ $this->println('Running migrations...');
+
+ $result = $this->runner->apply();
+
+ if ($result->hasApplied()) {
+ foreach ($result->getApplied() as $change) {
+ $this->success('Applied: ' . $change->getName());
+ }
+ }
+
+ if ($result->hasSkipped()) {
+ foreach ($result->getSkipped() as $item) {
+ $this->warning('Skipped: ' . $item['change']->getName() . ' (' . $item['reason'] . ')');
+ }
+ }
+
+ if ($result->hasFailed()) {
+ foreach ($result->getFailed() as $item) {
+ $this->error('Failed: ' . $item['change']->getName());
+ $this->println(' Error: ' . $item['error']->getMessage());
+ }
+ }
+
+ $this->info('Applied: ' . $result->count() . ' migrations');
+ $this->info('Time: ' . round($result->getTotalTime(), 2) . 'ms');
+
+ return $result->hasFailed() ? 1 : 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandNewTest.php b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandNewTest.php
new file mode 100644
index 000000000..0d006ec09
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandNewTest.php
@@ -0,0 +1,92 @@
+removeAllDBConnections();
+
+ $output = $this->executeMultiCommand([
+ RunMigrationsCommandNew::class
+ ]);
+
+ $this->assertEquals([
+ "Info: No database connections configured.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRunWithInvalidConnection() {
+ $output = $this->executeMultiCommand([
+ RunMigrationsCommandNew::class,
+ '--connection' => 'invalid-connection'
+ ]);
+
+ $this->assertEquals([
+ "Error: Connection 'invalid-connection' not found.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRunWithNoMigrations() {
+ $output = $this->executeMultiCommand([
+ RunMigrationsCommandNew::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $this->assertEquals([
+ "Info: No migrations found.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function cleanupMigrations(): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (is_dir($dir)) {
+ foreach (glob($dir.DS.'*.php') as $file) {
+ if (basename($file) !== '.gitkeep') {
+ unlink($file);
+ }
+ }
+ }
+ }
+
+ private function setupTestConnection(): void {
+ $this->testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
+ $this->testConnection->setName('test-connection');
+ App::getConfig()->addOrUpdateDBConnection($this->testConnection);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->setupTestConnection();
+ $this->cleanupMigrations();
+ }
+
+ protected function tearDown(): void {
+ $this->cleanupMigrations();
+ App::getConfig()->removeAllDBConnections();
+ parent::tearDown();
+ }
+}
From 56b5443e92eb4de7d18e370935fcbd58bf4cce54 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 23:21:56 +0300
Subject: [PATCH 65/88] refactor: Move `rollback` Migrations
---
WebFiori/Framework/App.php | 1 +
.../Commands/RollbackMigrationsCommand.php | 115 ++++++++++++++++
.../Cli/RollbackMigrationsCommandTest.php | 127 ++++++++++++++++++
3 files changed, 243 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/RollbackMigrationsCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/RollbackMigrationsCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index ef4d02cae..48ed65853 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -369,6 +369,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew',
+ '\\WebFiori\\Framework\\Cli\\Commands\\RollbackMigrationsCommand',
];
foreach ($commands as $c) {
diff --git a/WebFiori/Framework/Cli/Commands/RollbackMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/RollbackMigrationsCommand.php
new file mode 100644
index 000000000..087b24db9
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/RollbackMigrationsCommand.php
@@ -0,0 +1,115 @@
+getConnection();
+ if ($connection === null) {
+ return 1;
+ }
+
+ $env = $this->getArgValue('--env') ?? 'dev';
+ $this->runner = new SchemaRunner($connection, $env);
+
+ // Discover migrations
+ $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
+ $namespace = APP_DIR.'\\Database\\Migrations';
+ $this->runner->discoverFromPath($migrationsPath, $namespace);
+
+ return $this->rollback();
+
+ } catch (Throwable $e) {
+ $this->error('An exception was thrown.');
+ $this->println('Message: ' . $e->getMessage());
+ $this->println('File: ' . $e->getFile() . ':' . $e->getLine());
+ return 1;
+ }
+ }
+
+ private function getConnection(): ?ConnectionInfo {
+ $connections = App::getConfig()->getDBConnections();
+
+ if (empty($connections)) {
+ $this->info('No database connections configured.');
+ return null;
+ }
+
+ $connectionName = $this->getArgValue('--connection');
+
+ if ($connectionName !== null) {
+ $connection = App::getConfig()->getDBConnection($connectionName);
+ if ($connection === null) {
+ $this->error("Connection '$connectionName' not found.");
+ return null;
+ }
+ return $connection;
+ }
+
+ return CLIUtils::getConnectionName($this);
+ }
+
+ private function rollback(): int {
+ try {
+ if ($this->isArgProvided('--all')) {
+ $this->println('Rolling back all migrations...');
+ $rolled = $this->runner->rollbackUpTo(null);
+ } else if ($this->isArgProvided('--batch')) {
+ $batch = (int)$this->getArgValue('--batch');
+ $this->println("Rolling back batch $batch...");
+ $rolled = $this->runner->rollbackBatch($batch);
+ } else {
+ $this->println('Rolling back last batch...');
+ $rolled = $this->runner->rollbackLastBatch();
+ }
+
+ if (empty($rolled)) {
+ $this->info('No migrations to rollback.');
+ } else {
+ foreach ($rolled as $change) {
+ $this->success('Rolled back: ' . $change->getName());
+ }
+ $this->info('Total rolled back: ' . count($rolled));
+ }
+
+ return 0;
+ } catch (Throwable $e) {
+ $this->error('Rollback failed: ' . $e->getMessage());
+ return 1;
+ }
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/RollbackMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/RollbackMigrationsCommandTest.php
new file mode 100644
index 000000000..dea7fa767
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/RollbackMigrationsCommandTest.php
@@ -0,0 +1,127 @@
+removeAllDBConnections();
+
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class
+ ]);
+
+ $this->assertEquals([
+ "Info: No database connections configured.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRollbackWithInvalidConnection() {
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class,
+ '--connection' => 'invalid-connection'
+ ]);
+
+ $this->assertEquals([
+ "Error: Connection 'invalid-connection' not found.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRollbackWithNoMigrations() {
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $this->assertEquals([
+ "Rolling back last batch...\n",
+ "Info: No migrations to rollback.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRollbackAllWithNoMigrations() {
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class,
+ '--connection' => 'test-connection',
+ '--all'
+ ]);
+
+ $this->assertEquals([
+ "Rolling back all migrations...\n",
+ "Info: No migrations to rollback.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRollbackBatchWithNoMigrations() {
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class,
+ '--connection' => 'test-connection',
+ '--batch' => '1'
+ ]);
+
+ $this->assertEquals([
+ "Rolling back batch 1...\n",
+ "Info: No migrations to rollback.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function cleanupMigrations(): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (is_dir($dir)) {
+ foreach (glob($dir.DS.'*.php') as $file) {
+ if (basename($file) !== '.gitkeep') {
+ unlink($file);
+ }
+ }
+ }
+ }
+
+ private function setupTestConnection(): void {
+ $this->testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
+ $this->testConnection->setName('test-connection');
+ App::getConfig()->addOrUpdateDBConnection($this->testConnection);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->setupTestConnection();
+ $this->cleanupMigrations();
+ }
+
+ protected function tearDown(): void {
+ $this->cleanupMigrations();
+ App::getConfig()->removeAllDBConnections();
+ parent::tearDown();
+ }
+}
From 9ac070d9af8cf79210580c09dc119890027c2862 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 23:25:28 +0300
Subject: [PATCH 66/88] refactor: Move `init` Migrations
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/InitMigrationsCommand.php | 90 +++++++++++++++++++
.../Tests/Cli/InitMigrationsCommandTest.php | 79 ++++++++++++++++
3 files changed, 170 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/InitMigrationsCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 48ed65853..5fb96fb09 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -370,6 +370,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew',
'\\WebFiori\\Framework\\Cli\\Commands\\RollbackMigrationsCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
];
foreach ($commands as $c) {
diff --git a/WebFiori/Framework/Cli/Commands/InitMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/InitMigrationsCommand.php
new file mode 100644
index 000000000..30620fa8b
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/InitMigrationsCommand.php
@@ -0,0 +1,90 @@
+getConnection();
+ if ($connection === null) {
+ return 1;
+ }
+
+ $env = $this->getArgValue('--env') ?? 'dev';
+ $this->runner = new SchemaRunner($connection, $env);
+
+ return $this->initTable();
+
+ } catch (Throwable $e) {
+ $this->error('An exception was thrown.');
+ $this->println('Message: ' . $e->getMessage());
+ $this->println('File: ' . $e->getFile() . ':' . $e->getLine());
+ return 1;
+ }
+ }
+
+ private function getConnection(): ?ConnectionInfo {
+ $connections = App::getConfig()->getDBConnections();
+
+ if (empty($connections)) {
+ $this->info('No database connections configured.');
+ return null;
+ }
+
+ $connectionName = $this->getArgValue('--connection');
+
+ if ($connectionName !== null) {
+ $connection = App::getConfig()->getDBConnection($connectionName);
+ if ($connection === null) {
+ $this->error("Connection '$connectionName' not found.");
+ return null;
+ }
+ return $connection;
+ }
+
+ return CLIUtils::getConnectionName($this);
+ }
+
+ private function initTable(): int {
+ try {
+ $this->println('Creating migrations tracking table...');
+ $this->runner->createSchemaTable();
+ $this->success('Migrations table created successfully.');
+ return 0;
+ } catch (Throwable $e) {
+ $this->error('Failed to create migrations table: ' . $e->getMessage());
+ return 1;
+ }
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php
new file mode 100644
index 000000000..71a131bfa
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php
@@ -0,0 +1,79 @@
+removeAllDBConnections();
+
+ $output = $this->executeMultiCommand([
+ InitMigrationsCommand::class
+ ]);
+
+ $this->assertEquals([
+ "Info: No database connections configured.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testInitWithInvalidConnection() {
+ $output = $this->executeMultiCommand([
+ InitMigrationsCommand::class,
+ '--connection' => 'invalid-connection'
+ ]);
+
+ $this->assertEquals([
+ "Error: Connection 'invalid-connection' not found.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testInitMigrationsTable() {
+ $output = $this->executeMultiCommand([
+ InitMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $this->assertEquals([
+ "Creating migrations tracking table...\n",
+ "Success: Migrations table created successfully.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function setupTestConnection(): void {
+ $this->testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
+ $this->testConnection->setName('test-connection');
+ App::getConfig()->addOrUpdateDBConnection($this->testConnection);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->setupTestConnection();
+ }
+
+ protected function tearDown(): void {
+ App::getConfig()->removeAllDBConnections();
+ parent::tearDown();
+ }
+}
From a4eb634590db2c7118c36fe9c9db415b1e668219 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 23:26:02 +0300
Subject: [PATCH 67/88] refactor: Move `dry-run` Migrations
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/DryRunMigrationsCommand.php | 110 ++++++++++++++++++
.../Tests/Cli/DryRunMigrationsCommandTest.php | 92 +++++++++++++++
3 files changed, 203 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/DryRunMigrationsCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 5fb96fb09..5c0db99bc 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -371,6 +371,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew',
'\\WebFiori\\Framework\\Cli\\Commands\\RollbackMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\DryRunMigrationsCommand',
];
foreach ($commands as $c) {
diff --git a/WebFiori/Framework/Cli/Commands/DryRunMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/DryRunMigrationsCommand.php
new file mode 100644
index 000000000..a595e938c
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/DryRunMigrationsCommand.php
@@ -0,0 +1,110 @@
+getConnection();
+ if ($connection === null) {
+ return 1;
+ }
+
+ $env = $this->getArgValue('--env') ?? 'dev';
+ $this->runner = new SchemaRunner($connection, $env);
+
+ // Discover migrations
+ $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
+ $namespace = APP_DIR.'\\Database\\Migrations';
+ $count = $this->runner->discoverFromPath($migrationsPath, $namespace);
+
+ if ($count === 0) {
+ $this->info('No migrations found.');
+ return 0;
+ }
+
+ return $this->dryRun();
+
+ } catch (Throwable $e) {
+ $this->error('An exception was thrown.');
+ $this->println('Message: ' . $e->getMessage());
+ $this->println('File: ' . $e->getFile() . ':' . $e->getLine());
+ return 1;
+ }
+ }
+
+ private function getConnection(): ?ConnectionInfo {
+ $connections = App::getConfig()->getDBConnections();
+
+ if (empty($connections)) {
+ $this->info('No database connections configured.');
+ return null;
+ }
+
+ $connectionName = $this->getArgValue('--connection');
+
+ if ($connectionName !== null) {
+ $connection = App::getConfig()->getDBConnection($connectionName);
+ if ($connection === null) {
+ $this->error("Connection '$connectionName' not found.");
+ return null;
+ }
+ return $connection;
+ }
+
+ return CLIUtils::getConnectionName($this);
+ }
+
+ private function dryRun(): int {
+ $pending = $this->runner->getPendingChanges(true);
+
+ if (empty($pending)) {
+ $this->info('No pending migrations.');
+ return 0;
+ }
+
+ $this->println('Pending migrations:');
+ foreach ($pending as $item) {
+ $this->println(' - ' . $item['change']->getName());
+ if (!empty($item['queries'])) {
+ $this->println(' Queries:');
+ foreach ($item['queries'] as $query) {
+ $this->println(' ' . $query);
+ }
+ }
+ }
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
new file mode 100644
index 000000000..aae8ec560
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
@@ -0,0 +1,92 @@
+removeAllDBConnections();
+
+ $output = $this->executeMultiCommand([
+ DryRunMigrationsCommand::class
+ ]);
+
+ $this->assertEquals([
+ "Info: No database connections configured.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testDryRunWithInvalidConnection() {
+ $output = $this->executeMultiCommand([
+ DryRunMigrationsCommand::class,
+ '--connection' => 'invalid-connection'
+ ]);
+
+ $this->assertEquals([
+ "Error: Connection 'invalid-connection' not found.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testDryRunWithNoMigrations() {
+ $output = $this->executeMultiCommand([
+ DryRunMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $this->assertEquals([
+ "Info: No migrations found.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function cleanupMigrations(): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (is_dir($dir)) {
+ foreach (glob($dir.DS.'*.php') as $file) {
+ if (basename($file) !== '.gitkeep') {
+ unlink($file);
+ }
+ }
+ }
+ }
+
+ private function setupTestConnection(): void {
+ $this->testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
+ $this->testConnection->setName('test-connection');
+ App::getConfig()->addOrUpdateDBConnection($this->testConnection);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->setupTestConnection();
+ $this->cleanupMigrations();
+ }
+
+ protected function tearDown(): void {
+ $this->cleanupMigrations();
+ App::getConfig()->removeAllDBConnections();
+ parent::tearDown();
+ }
+}
From cbd6b298b290676d03227253ac21598ded329100 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 23:28:05 +0300
Subject: [PATCH 68/88] refactor: Move `status` Migrations
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/MigrationsStatusCommand.php | 115 ++++++++++++++++++
.../Tests/Cli/MigrationsStatusCommandTest.php | 92 ++++++++++++++
3 files changed, 208 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 5c0db99bc..37bcaa6a6 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -372,6 +372,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\RollbackMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\DryRunMigrationsCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\MigrationsStatusCommand',
];
foreach ($commands as $c) {
diff --git a/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php b/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
new file mode 100644
index 000000000..052e55ccb
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
@@ -0,0 +1,115 @@
+getConnection();
+ if ($connection === null) {
+ return 1;
+ }
+
+ $env = $this->getArgValue('--env') ?? 'dev';
+ $this->runner = new SchemaRunner($connection, $env);
+
+ // Discover migrations
+ $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
+ $namespace = APP_DIR.'\\Database\\Migrations';
+ $count = $this->runner->discoverFromPath($migrationsPath, $namespace);
+
+ if ($count === 0) {
+ $this->info('No migrations found.');
+ return 0;
+ }
+
+ return $this->showStatus();
+
+ } catch (Throwable $e) {
+ $this->error('An exception was thrown.');
+ $this->println('Message: ' . $e->getMessage());
+ $this->println('File: ' . $e->getFile() . ':' . $e->getLine());
+ return 1;
+ }
+ }
+
+ private function getConnection(): ?ConnectionInfo {
+ $connections = App::getConfig()->getDBConnections();
+
+ if (empty($connections)) {
+ $this->info('No database connections configured.');
+ return null;
+ }
+
+ $connectionName = $this->getArgValue('--connection');
+
+ if ($connectionName !== null) {
+ $connection = App::getConfig()->getDBConnection($connectionName);
+ if ($connection === null) {
+ $this->error("Connection '$connectionName' not found.");
+ return null;
+ }
+ return $connection;
+ }
+
+ return CLIUtils::getConnectionName($this);
+ }
+
+ private function showStatus(): int {
+ $pending = $this->runner->getPendingChanges(false);
+ $applied = $this->runner->getAppliedChanges();
+
+ if (!empty($applied)) {
+ $this->println('Applied migrations:');
+ foreach ($applied as $change) {
+ $this->success(' - ' . $change->getName());
+ }
+ } else {
+ $this->info('No applied migrations.');
+ }
+
+ $this->println('');
+
+ if (!empty($pending)) {
+ $this->println('Pending migrations:');
+ foreach ($pending as $item) {
+ $this->warning(' - ' . $item['change']->getName());
+ }
+ } else {
+ $this->info('No pending migrations.');
+ }
+
+ return 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php
new file mode 100644
index 000000000..faae8db58
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php
@@ -0,0 +1,92 @@
+removeAllDBConnections();
+
+ $output = $this->executeMultiCommand([
+ MigrationsStatusCommand::class
+ ]);
+
+ $this->assertEquals([
+ "Info: No database connections configured.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testStatusWithInvalidConnection() {
+ $output = $this->executeMultiCommand([
+ MigrationsStatusCommand::class,
+ '--connection' => 'invalid-connection'
+ ]);
+
+ $this->assertEquals([
+ "Error: Connection 'invalid-connection' not found.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testStatusWithNoMigrations() {
+ $output = $this->executeMultiCommand([
+ MigrationsStatusCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $this->assertEquals([
+ "Info: No migrations found.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function cleanupMigrations(): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (is_dir($dir)) {
+ foreach (glob($dir.DS.'*.php') as $file) {
+ if (basename($file) !== '.gitkeep') {
+ unlink($file);
+ }
+ }
+ }
+ }
+
+ private function setupTestConnection(): void {
+ $this->testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
+ $this->testConnection->setName('test-connection');
+ App::getConfig()->addOrUpdateDBConnection($this->testConnection);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->setupTestConnection();
+ $this->cleanupMigrations();
+ }
+
+ protected function tearDown(): void {
+ $this->cleanupMigrations();
+ App::getConfig()->removeAllDBConnections();
+ parent::tearDown();
+ }
+}
From e0d64902f1feeba5525e5248379a8e0ffcb05517 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 23:30:15 +0300
Subject: [PATCH 69/88] test: Move test Cases
---
.../Tests/Cli/DryRunMigrationsCommandTest.php | 45 ++++++
.../Tests/Cli/RunMigrationsCommandTest.php | 149 ------------------
2 files changed, 45 insertions(+), 149 deletions(-)
delete mode 100644 tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php
diff --git a/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
index aae8ec560..bcd07583b 100644
--- a/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
@@ -60,6 +60,51 @@ public function testDryRunWithNoMigrations() {
$this->assertEquals(0, $this->getExitCode());
}
+ /**
+ * @test
+ */
+ public function testDryRunWithPendingMigration() {
+ $this->createTestMigration('TestMigration');
+
+ $output = $this->executeMultiCommand([
+ DryRunMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Pending migrations:', $outputStr);
+ $this->assertStringContainsString('TestMigration', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function createTestMigration(string $name): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ $content = <<createTestMigration('TestMigration');
-
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection',
- '--dry-run'
- ]);
-
- // Check if output contains expected text
- $outputStr = implode('', $output);
- $this->assertStringContainsString('Pending migrations:', $outputStr);
- $this->assertStringContainsString('TestMigration', $outputStr);
- $this->assertEquals(0, $this->getExitCode());
- }
-
- /**
- * @test
- */
- public function testExecWithInvalidConnection(): void {
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'invalid-connection'
- ]);
-
- $this->assertContains("Error: Connection 'invalid-connection' not found.\n", $output);
- $this->assertEquals(1, $this->getExitCode());
- }
-
- /**
- * @test
- */
- public function testExecWithNoConnections(): void {
- App::getConfig()->removeAllDBConnections();
-
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class
- ]);
-
- $this->assertContains("Info: No database connections configured.\n", $output);
- $this->assertEquals(1, $this->getExitCode());
- }
-
- /**
- * @test
- */
- public function testInitializeMigrationsTable(): void {
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection',
- '--init'
- ]);
-
- $this->assertContains("Creating migrations tracking table...\n", $output);
- $this->assertContains("Success: Migrations table created successfully.\n", $output);
- $this->assertEquals(0, $this->getExitCode());
- }
-
- /**
- * @test
- */
- public function testRunWithNoMigrations(): void {
- $output = $this->executeMultiCommand([
- RunMigrationsCommand::class,
- '--connection' => 'test-connection'
- ]);
-
- $this->assertContains("Info: No migrations found.\n", $output);
- $this->assertEquals(0, $this->getExitCode());
- }
-
- private function cleanupMigrations(): void {
- $dir = APP_PATH.'Database'.DS.'Migrations';
-
- if (is_dir($dir)) {
- foreach (glob($dir.DS.'*.php') as $file) {
- if (basename($file) !== '.gitkeep') {
- unlink($file);
- }
- }
- }
- }
-
- private function createTestMigration(string $name): void {
- $dir = APP_PATH.'Database'.DS.'Migrations';
-
- if (!is_dir($dir)) {
- mkdir($dir, 0755, true);
- }
-
- $content = <<testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
- $this->testConnection->setName('test-connection');
- App::getConfig()->addOrUpdateDBConnection($this->testConnection);
- }
-
- protected function setUp(): void {
- parent::setUp();
- $this->setupTestConnection();
- $this->cleanupMigrations();
- }
-
- protected function tearDown(): void {
- $this->cleanupMigrations();
- App::getConfig()->removeAllDBConnections();
- parent::tearDown();
- }
-}
From 3781a6a398a51f1b2992fcae95a8d36cb1c777f3 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 23:40:35 +0300
Subject: [PATCH 70/88] refactor: Remove Old Migrations Command
---
WebFiori/Framework/App.php | 1 -
.../Cli/Commands/RunMigrationsCommand.php | 199 ------------------
.../Framework/Tests/Cli/HelpCommandTest.php | 6 +-
3 files changed, 5 insertions(+), 201 deletions(-)
delete mode 100644 WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 37bcaa6a6..170b88979 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -367,7 +367,6 @@ public static function getRunner() : Runner {
- '\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew',
'\\WebFiori\\Framework\\Cli\\Commands\\RollbackMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
diff --git a/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php
deleted file mode 100644
index 017bcb869..000000000
--- a/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php
+++ /dev/null
@@ -1,199 +0,0 @@
-getConnection();
- if ($connection === null) {
- return 1;
- }
-
- $env = $this->getArgValue('--env') ?? 'dev';
- $this->runner = new SchemaRunner($connection, $env);
-
- // Discover migrations
- $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
- $namespace = APP_DIR.'\\Database\\Migrations';
- $count = $this->runner->discoverFromPath($migrationsPath, $namespace);
-
- if ($count === 0 && !$this->isArgProvided('--init')) {
- $this->info('No migrations found.');
- return 0;
- }
-
- if ($this->isArgProvided('--init')) {
- return $this->initTable();
- }
-
- if ($this->isArgProvided('--rollback')) {
- return $this->rollback();
- }
-
- if ($this->isArgProvided('--dry-run')) {
- return $this->dryRun();
- }
-
- return $this->runMigrations();
-
- } catch (Throwable $e) {
- $this->error('An exception was thrown.');
- $this->println('Message: ' . $e->getMessage());
- $this->println('File: ' . $e->getFile() . ':' . $e->getLine());
- return 1;
- }
- }
-
- private function getConnection(): ?ConnectionInfo {
- $connections = App::getConfig()->getDBConnections();
-
- if (empty($connections)) {
- $this->info('No database connections configured.');
- return null;
- }
-
- $connectionName = $this->getArgValue('--connection');
-
- if ($connectionName !== null) {
- $connection = App::getConfig()->getDBConnection($connectionName);
- if ($connection === null) {
- $this->error("Connection '$connectionName' not found.");
- return null;
- }
- return $connection;
- }
-
- return CLIUtils::getConnectionName($this);
- }
-
- private function initTable(): int {
- try {
- $this->println('Creating migrations tracking table...');
- $this->runner->createSchemaTable();
- $this->success('Migrations table created successfully.');
- return 0;
- } catch (Throwable $e) {
- $this->error('Failed to create migrations table: ' . $e->getMessage());
- return 1;
- }
- }
-
- private function rollback(): int {
- try {
- if ($this->isArgProvided('--all')) {
- $this->println('Rolling back all migrations...');
- $rolled = $this->runner->rollbackUpTo(null);
- } else if ($this->isArgProvided('--batch')) {
- $batch = (int)$this->getArgValue('--batch');
- $this->println("Rolling back batch $batch...");
- $rolled = $this->runner->rollbackBatch($batch);
- } else {
- $this->println('Rolling back last batch...');
- $rolled = $this->runner->rollbackLastBatch();
- }
-
- if (empty($rolled)) {
- $this->info('No migrations to rollback.');
- } else {
- foreach ($rolled as $change) {
- $this->success('Rolled back: ' . $change->getName());
- }
- $this->info('Total rolled back: ' . count($rolled));
- }
-
- return 0;
- } catch (Throwable $e) {
- $this->error('Rollback failed: ' . $e->getMessage());
- return 1;
- }
- }
-
- private function dryRun(): int {
- $pending = $this->runner->getPendingChanges(true);
-
- if (empty($pending)) {
- $this->info('No pending migrations.');
- return 0;
- }
-
- $this->println('Pending migrations:');
- foreach ($pending as $item) {
- $this->println(' - ' . $item['change']->getName());
- if (!empty($item['queries'])) {
- $this->println(' Queries:');
- foreach ($item['queries'] as $query) {
- $this->println(' ' . $query);
- }
- }
- }
-
- return 0;
- }
-
- private function runMigrations(): int {
- $this->println('Running migrations...');
-
- $result = $this->runner->apply();
-
- if ($result->hasApplied()) {
- foreach ($result->getApplied() as $change) {
- $this->success('Applied: ' . $change->getName());
- }
- }
-
- if ($result->hasSkipped()) {
- foreach ($result->getSkipped() as $item) {
- $this->warning('Skipped: ' . $item['change']->getName() . ' (' . $item['reason'] . ')');
- }
- }
-
- if ($result->hasFailed()) {
- foreach ($result->getFailed() as $item) {
- $this->error('Failed: ' . $item['change']->getName());
- $this->println(' Error: ' . $item['error']->getMessage());
- }
- }
-
- $this->info('Applied: ' . $result->count() . ' migrations');
- $this->info('Time: ' . round($result->getTotalTime(), 2) . 'ms');
-
- return $result->hasFailed() ? 1 : 0;
- }
-}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 501cc79ae..1102ede13 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -37,7 +37,11 @@ public function test00() {
- " migrations: Execute database migrations.\n",
+ " migrations:run: Execute pending database migrations.\n",
+ " migrations:rollback: Rollback database migrations.\n",
+ " migrations:ini: Create migrations tracking table.\n",
+ " migrations:dry-run: Preview pending migrations without executing.\n",
+ " migrations:status: Show migration status (applied and pending).\n",
], $this->executeMultiCommand([
'help',
]));
From 47d6a27d46cafae37eaf4fae9bec5e67288a2da3 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Mon, 9 Feb 2026 23:56:43 +0300
Subject: [PATCH 71/88] test: Added More Migrations Tests
---
.../Cli/Commands/MigrationsStatusCommand.php | 6 +-
.../Cli/Commands/RunMigrationsCommandNew.php | 17 ++-
.../Tests/Cli/DryRunMigrationsCommandTest.php | 71 ++++++++++
.../Tests/Cli/InitMigrationsCommandTest.php | 40 ++++++
.../Tests/Cli/MigrationsStatusCommandTest.php | 119 ++++++++++++++++
.../Cli/RollbackMigrationsCommandTest.php | 131 ++++++++++++++++++
.../Tests/Cli/RunMigrationsCommandNewTest.php | 120 ++++++++++++++++
7 files changed, 496 insertions(+), 8 deletions(-)
diff --git a/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php b/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
index 052e55ccb..49c8e7fb8 100644
--- a/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
+++ b/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php
@@ -87,8 +87,12 @@ private function getConnection(): ?ConnectionInfo {
}
private function showStatus(): int {
+ $allChanges = $this->runner->getChanges();
$pending = $this->runner->getPendingChanges(false);
- $applied = $this->runner->getAppliedChanges();
+
+ // Separate applied and pending
+ $pendingNames = array_map(fn($item) => $item['change']->getName(), $pending);
+ $applied = array_filter($allChanges, fn($change) => !in_array($change->getName(), $pendingNames));
if (!empty($applied)) {
$this->println('Applied migrations:');
diff --git a/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php b/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
index d1a7e72c0..0dca325b5 100644
--- a/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
+++ b/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php
@@ -91,20 +91,23 @@ private function runMigrations(): int {
$result = $this->runner->apply();
- if ($result->hasApplied()) {
- foreach ($result->getApplied() as $change) {
+ $applied = $result->getApplied();
+ if (!empty($applied)) {
+ foreach ($applied as $change) {
$this->success('Applied: ' . $change->getName());
}
}
- if ($result->hasSkipped()) {
- foreach ($result->getSkipped() as $item) {
+ $skipped = $result->getSkipped();
+ if (!empty($skipped)) {
+ foreach ($skipped as $item) {
$this->warning('Skipped: ' . $item['change']->getName() . ' (' . $item['reason'] . ')');
}
}
- if ($result->hasFailed()) {
- foreach ($result->getFailed() as $item) {
+ $failed = $result->getFailed();
+ if (!empty($failed)) {
+ foreach ($failed as $item) {
$this->error('Failed: ' . $item['change']->getName());
$this->println(' Error: ' . $item['error']->getMessage());
}
@@ -113,6 +116,6 @@ private function runMigrations(): int {
$this->info('Applied: ' . $result->count() . ' migrations');
$this->info('Time: ' . round($result->getTotalTime(), 2) . 'ms');
- return $result->hasFailed() ? 1 : 0;
+ return !empty($failed) ? 1 : 0;
}
}
diff --git a/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
index bcd07583b..d702f8dd5 100644
--- a/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
@@ -77,6 +77,77 @@ public function testDryRunWithPendingMigration() {
$this->assertEquals(0, $this->getExitCode());
}
+ /**
+ * @test
+ */
+ public function testDryRunWithCustomEnv() {
+ $this->createTestMigration('EnvTestMigration');
+
+ $output = $this->executeMultiCommand([
+ DryRunMigrationsCommand::class,
+ '--connection' => 'test-connection',
+ '--env' => 'staging'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Pending migrations:', $outputStr);
+ $this->assertStringContainsString('EnvTestMigration', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testDryRunShowsQueries() {
+ $this->createTestMigrationWithSchema('QueryTestMigration');
+
+ $output = $this->executeMultiCommand([
+ DryRunMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Pending migrations:', $outputStr);
+ $this->assertStringContainsString('QueryTestMigration', $outputStr);
+ // Queries section may or may not appear depending on migration content
+ // Just verify the migration is listed
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function createTestMigrationWithSchema(string $name): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ $content = <<addColumns([
+ 'id' => ['type' => 'int', 'primary' => true, 'auto-increment' => true],
+ 'name' => ['type' => 'varchar', 'size' => 100]
+ ]);
+ \$db->table(\$table);
+ }
+
+ public function down(Database \$db): void {
+ \$db->table('test_table')->drop();
+ }
+}
+PHP;
+
+ file_put_contents($dir.DS.$name.'.php', $content);
+ }
+
private function createTestMigration(string $name): void {
$dir = APP_PATH.'Database'.DS.'Migrations';
diff --git a/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php
index 71a131bfa..47218bdf1 100644
--- a/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php
@@ -61,6 +61,46 @@ public function testInitMigrationsTable() {
$this->assertEquals(0, $this->getExitCode());
}
+ /**
+ * @test
+ */
+ public function testInitWithCustomEnv() {
+ $output = $this->executeMultiCommand([
+ InitMigrationsCommand::class,
+ '--connection' => 'test-connection',
+ '--env' => 'staging'
+ ]);
+
+ $this->assertEquals([
+ "Creating migrations tracking table...\n",
+ "Success: Migrations table created successfully.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testInitTableAlreadyExists() {
+ // Create table first
+ $this->executeMultiCommand([
+ InitMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ // Try to create again
+ $output = $this->executeMultiCommand([
+ InitMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $this->assertEquals([
+ "Creating migrations tracking table...\n",
+ "Success: Migrations table created successfully.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
private function setupTestConnection(): void {
$this->testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
$this->testConnection->setName('test-connection');
diff --git a/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php
index faae8db58..5e04b7e4c 100644
--- a/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php
@@ -60,6 +60,125 @@ public function testStatusWithNoMigrations() {
$this->assertEquals(0, $this->getExitCode());
}
+ /**
+ * @test
+ */
+ public function testStatusWithPendingMigrationsOnly() {
+ $this->createTestMigration('TestMigration1');
+ $this->createTestMigration('TestMigration2');
+
+ $output = $this->executeMultiCommand([
+ MigrationsStatusCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Info: No applied migrations.', $outputStr);
+ $this->assertStringContainsString('Pending migrations:', $outputStr);
+ $this->assertStringContainsString('TestMigration1', $outputStr);
+ $this->assertStringContainsString('TestMigration2', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testStatusWithAppliedMigrationsOnly() {
+ $this->createTestMigration('AppliedMigration1');
+ $this->initAndRunMigrations();
+
+ $output = $this->executeMultiCommand([
+ MigrationsStatusCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Applied migrations:', $outputStr);
+ $this->assertStringContainsString('AppliedMigration1', $outputStr);
+ $this->assertStringContainsString('Info: No pending migrations.', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testStatusWithBothAppliedAndPending() {
+ $this->createTestMigration('AppliedMigration1');
+ $this->initAndRunMigrations();
+
+ $this->createTestMigration('PendingMigration1');
+
+ $output = $this->executeMultiCommand([
+ MigrationsStatusCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Applied migrations:', $outputStr);
+ $this->assertStringContainsString('AppliedMigration1', $outputStr);
+ $this->assertStringContainsString('Pending migrations:', $outputStr);
+ $this->assertStringContainsString('PendingMigration1', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testStatusWithCustomEnv() {
+ $this->createTestMigration('TestMigration1');
+
+ $output = $this->executeMultiCommand([
+ MigrationsStatusCommand::class,
+ '--connection' => 'test-connection',
+ '--env' => 'staging'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Pending migrations:', $outputStr);
+ $this->assertStringContainsString('TestMigration1', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function initAndRunMigrations(): void {
+ $this->executeMultiCommand([
+ 'WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
+ '--connection' => 'test-connection'
+ ]);
+
+ $this->executeMultiCommand([
+ 'WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew',
+ '--connection' => 'test-connection'
+ ]);
+ }
+
+ private function createTestMigration(string $name): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ $content = <<assertEquals(0, $this->getExitCode());
}
+ /**
+ * @test
+ */
+ public function testRollbackLastBatchWithMigrations() {
+ $this->createTestMigration('RollbackTest1');
+ $this->initAndRunMigrations();
+
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Rolling back last batch...', $outputStr);
+ $this->assertStringContainsString('Rolled back: App\\Database\\Migrations\\RollbackTest1', $outputStr);
+ $this->assertStringContainsString('Info: Total rolled back: 1', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRollbackSpecificBatchWithMigrations() {
+ $this->createTestMigration('Batch1Migration');
+ $this->initAndRunMigrations();
+
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Rolling back last batch...', $outputStr);
+ $this->assertStringContainsString('Rolled back: App\\Database\\Migrations\\Batch1Migration', $outputStr);
+ $this->assertStringContainsString('Info: Total rolled back: 1', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRollbackAllWithMigrations() {
+ $this->createTestMigration('Migration1');
+ $this->createTestMigration('Migration2');
+ $this->initAndRunMigrations();
+
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class,
+ '--connection' => 'test-connection',
+ '--all'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Rolling back all migrations...', $outputStr);
+ $this->assertStringContainsString('Rolled back: App\\Database\\Migrations\\Migration1', $outputStr);
+ $this->assertStringContainsString('Rolled back: App\\Database\\Migrations\\Migration2', $outputStr);
+ $this->assertStringContainsString('Info: Total rolled back: 2', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRollbackWithCustomEnv() {
+ $this->createTestMigration('EnvTest1');
+ $this->initAndRunMigrations('staging');
+
+ $output = $this->executeMultiCommand([
+ RollbackMigrationsCommand::class,
+ '--connection' => 'test-connection',
+ '--env' => 'staging'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Rolling back last batch...', $outputStr);
+ $this->assertStringContainsString('Rolled back: App\\Database\\Migrations\\EnvTest1', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function initAndRunMigrations(string $env = 'dev'): void {
+ $args = [
+ 'WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
+ '--connection' => 'test-connection'
+ ];
+
+ if ($env !== 'dev') {
+ $args['--env'] = $env;
+ }
+
+ $this->executeMultiCommand($args);
+
+ $args = [
+ 'WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew',
+ '--connection' => 'test-connection'
+ ];
+
+ if ($env !== 'dev') {
+ $args['--env'] = $env;
+ }
+
+ $this->executeMultiCommand($args);
+ }
+
+ private function createTestMigration(string $name): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ $content = <<assertEquals(0, $this->getExitCode());
}
+ /**
+ * @test
+ */
+ public function testRunWithPendingMigrations() {
+ $this->createTestMigration('RunTest1');
+ $this->createTestMigration('RunTest2');
+ $this->initMigrations();
+
+ $output = $this->executeMultiCommand([
+ RunMigrationsCommandNew::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Running migrations...', $outputStr);
+ $this->assertStringContainsString('Applied: App\\Database\\Migrations\\RunTest1', $outputStr);
+ $this->assertStringContainsString('Applied: App\\Database\\Migrations\\RunTest2', $outputStr);
+ $this->assertStringContainsString('Info: Applied: 2 migrations', $outputStr);
+ $this->assertStringContainsString('Info: Time:', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRunWithAlreadyAppliedMigrations() {
+ $this->createTestMigration('AlreadyApplied');
+ $this->initMigrations();
+
+ // Run migrations first time
+ $this->executeMultiCommand([
+ RunMigrationsCommandNew::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ // Run again - should skip
+ $output = $this->executeMultiCommand([
+ RunMigrationsCommandNew::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Running migrations...', $outputStr);
+ $this->assertStringContainsString('Info: Applied: 0 migrations', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testRunWithCustomEnv() {
+ $this->createTestMigration('EnvTest');
+ $this->initMigrations('staging');
+
+ $output = $this->executeMultiCommand([
+ RunMigrationsCommandNew::class,
+ '--connection' => 'test-connection',
+ '--env' => 'staging'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Running migrations...', $outputStr);
+ $this->assertStringContainsString('Applied: App\\Database\\Migrations\\EnvTest', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function initMigrations(string $env = 'dev'): void {
+ $args = [
+ 'WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
+ '--connection' => 'test-connection'
+ ];
+
+ if ($env !== 'dev') {
+ $args['--env'] = $env;
+ }
+
+ $this->executeMultiCommand($args);
+ }
+
+ private function createTestMigration(string $name): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ $content = <<cleanupMigrations();
+ $this->dropSchemaTable();
App::getConfig()->removeAllDBConnections();
parent::tearDown();
}
+
+ private function dropSchemaTable(): void {
+ try {
+ $connection = App::getConfig()->getDBConnection('test-connection');
+ if ($connection !== null) {
+ $runner = new \WebFiori\Database\Schema\SchemaRunner($connection);
+ $runner->dropSchemaTable();
+ }
+ } catch (\Throwable $e) {
+ // Ignore errors during cleanup
+ }
+ }
}
From d5f732b219375f8c795bc5ed4941d89f14f07594 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 10 Feb 2026 00:04:09 +0300
Subject: [PATCH 72/88] feat: Added `migrations:fresh` Command
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/FreshMigrationsCommand.php | 137 +++++++++++
.../Tests/Cli/FreshMigrationsCommandTest.php | 225 ++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
4 files changed, 364 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/FreshMigrationsCommand.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/FreshMigrationsCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 170b88979..4adceaae4 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -372,6 +372,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\DryRunMigrationsCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\MigrationsStatusCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\FreshMigrationsCommand',
];
foreach ($commands as $c) {
diff --git a/WebFiori/Framework/Cli/Commands/FreshMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/FreshMigrationsCommand.php
new file mode 100644
index 000000000..f6331f327
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/FreshMigrationsCommand.php
@@ -0,0 +1,137 @@
+getConnection();
+ if ($connection === null) {
+ return 1;
+ }
+
+ $env = $this->getArgValue('--env') ?? 'dev';
+ $this->runner = new SchemaRunner($connection, $env);
+
+ // Discover migrations
+ $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
+ $namespace = APP_DIR.'\\Database\\Migrations';
+ $count = $this->runner->discoverFromPath($migrationsPath, $namespace);
+
+ if ($count === 0) {
+ $this->info('No migrations found.');
+ return 0;
+ }
+
+ // Rollback all
+ $this->println('Rolling back all migrations...');
+ $rolled = $this->runner->rollbackUpTo(null);
+
+ if (!empty($rolled)) {
+ foreach ($rolled as $change) {
+ $this->success('Rolled back: ' . $change->getName());
+ }
+ $this->info('Total rolled back: ' . count($rolled));
+ } else {
+ $this->info('No migrations to rollback.');
+ }
+
+ $this->println('');
+
+ // Run all
+ return $this->runMigrations();
+
+ } catch (Throwable $e) {
+ $this->error('An exception was thrown.');
+ $this->println('Message: ' . $e->getMessage());
+ $this->println('File: ' . $e->getFile() . ':' . $e->getLine());
+ return 1;
+ }
+ }
+
+ private function getConnection(): ?ConnectionInfo {
+ $connections = App::getConfig()->getDBConnections();
+
+ if (empty($connections)) {
+ $this->info('No database connections configured.');
+ return null;
+ }
+
+ $connectionName = $this->getArgValue('--connection');
+
+ if ($connectionName !== null) {
+ $connection = App::getConfig()->getDBConnection($connectionName);
+ if ($connection === null) {
+ $this->error("Connection '$connectionName' not found.");
+ return null;
+ }
+ return $connection;
+ }
+
+ return CLIUtils::getConnectionName($this);
+ }
+
+ private function runMigrations(): int {
+ $this->println('Running migrations...');
+
+ $result = $this->runner->apply();
+
+ $applied = $result->getApplied();
+ if (!empty($applied)) {
+ foreach ($applied as $change) {
+ $this->success('Applied: ' . $change->getName());
+ }
+ }
+
+ $skipped = $result->getSkipped();
+ if (!empty($skipped)) {
+ foreach ($skipped as $item) {
+ $this->warning('Skipped: ' . $item['change']->getName() . ' (' . $item['reason'] . ')');
+ }
+ }
+
+ $failed = $result->getFailed();
+ if (!empty($failed)) {
+ foreach ($failed as $item) {
+ $this->error('Failed: ' . $item['change']->getName());
+ $this->println(' Error: ' . $item['error']->getMessage());
+ }
+ }
+
+ $this->info('Applied: ' . $result->count() . ' migrations');
+ $this->info('Time: ' . round($result->getTotalTime(), 2) . 'ms');
+
+ return !empty($failed) ? 1 : 0;
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/FreshMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/FreshMigrationsCommandTest.php
new file mode 100644
index 000000000..707f71e7c
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/FreshMigrationsCommandTest.php
@@ -0,0 +1,225 @@
+removeAllDBConnections();
+
+ $output = $this->executeMultiCommand([
+ FreshMigrationsCommand::class
+ ]);
+
+ $this->assertEquals([
+ "Info: No database connections configured.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testFreshWithInvalidConnection() {
+ $output = $this->executeMultiCommand([
+ FreshMigrationsCommand::class,
+ '--connection' => 'invalid-connection'
+ ]);
+
+ $this->assertEquals([
+ "Error: Connection 'invalid-connection' not found.\n"
+ ], $output);
+ $this->assertEquals(1, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testFreshWithNoMigrations() {
+ $output = $this->executeMultiCommand([
+ FreshMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $this->assertEquals([
+ "Info: No migrations found.\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testFreshWithMigrations() {
+ $this->createTestMigration('FreshTest1');
+ $this->createTestMigration('FreshTest2');
+ $this->initAndRunMigrations();
+
+ $output = $this->executeMultiCommand([
+ FreshMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Rolling back all migrations...', $outputStr);
+ $this->assertStringContainsString('Rolled back: App\\Database\\Migrations\\FreshTest1', $outputStr);
+ $this->assertStringContainsString('Rolled back: App\\Database\\Migrations\\FreshTest2', $outputStr);
+ $this->assertStringContainsString('Running migrations...', $outputStr);
+ $this->assertStringContainsString('Applied: App\\Database\\Migrations\\FreshTest1', $outputStr);
+ $this->assertStringContainsString('Applied: App\\Database\\Migrations\\FreshTest2', $outputStr);
+ $this->assertStringContainsString('Info: Applied: 2 migrations', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testFreshWithNoAppliedMigrations() {
+ $this->createTestMigration('FreshTest3');
+ $this->initMigrations();
+
+ $output = $this->executeMultiCommand([
+ FreshMigrationsCommand::class,
+ '--connection' => 'test-connection'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Rolling back all migrations...', $outputStr);
+ $this->assertStringContainsString('Info: No migrations to rollback.', $outputStr);
+ $this->assertStringContainsString('Running migrations...', $outputStr);
+ $this->assertStringContainsString('Applied: App\\Database\\Migrations\\FreshTest3', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ /**
+ * @test
+ */
+ public function testFreshWithCustomEnv() {
+ $this->createTestMigration('EnvFreshTest');
+ $this->initAndRunMigrations('staging');
+
+ $output = $this->executeMultiCommand([
+ FreshMigrationsCommand::class,
+ '--connection' => 'test-connection',
+ '--env' => 'staging'
+ ]);
+
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Rolling back all migrations...', $outputStr);
+ $this->assertStringContainsString('Running migrations...', $outputStr);
+ $this->assertStringContainsString('Applied: App\\Database\\Migrations\\EnvFreshTest', $outputStr);
+ $this->assertEquals(0, $this->getExitCode());
+ }
+
+ private function initMigrations(string $env = 'dev'): void {
+ $args = [
+ 'WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
+ '--connection' => 'test-connection'
+ ];
+
+ if ($env !== 'dev') {
+ $args['--env'] = $env;
+ }
+
+ $this->executeMultiCommand($args);
+ }
+
+ private function initAndRunMigrations(string $env = 'dev'): void {
+ $this->initMigrations($env);
+
+ $args = [
+ 'WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew',
+ '--connection' => 'test-connection'
+ ];
+
+ if ($env !== 'dev') {
+ $args['--env'] = $env;
+ }
+
+ $this->executeMultiCommand($args);
+ }
+
+ private function createTestMigration(string $name): void {
+ $dir = APP_PATH.'Database'.DS.'Migrations';
+
+ if (!is_dir($dir)) {
+ mkdir($dir, 0755, true);
+ }
+
+ $content = <<getDBConnection('test-connection');
+ if ($connection !== null) {
+ $runner = new \WebFiori\Database\Schema\SchemaRunner($connection);
+ $runner->dropSchemaTable();
+ }
+ } catch (\Throwable $e) {
+ // Ignore errors during cleanup
+ }
+ }
+
+ private function setupTestConnection(): void {
+ $this->testConnection = new ConnectionInfo('mysql', 'root', MYSQL_ROOT_PASSWORD, 'testing_db', '127.0.0.1', 3306);
+ $this->testConnection->setName('test-connection');
+ App::getConfig()->addOrUpdateDBConnection($this->testConnection);
+ }
+
+ protected function setUp(): void {
+ parent::setUp();
+ $this->setupTestConnection();
+ $this->cleanupMigrations();
+ }
+
+ protected function tearDown(): void {
+ $this->cleanupMigrations();
+ $this->dropSchemaTable();
+ App::getConfig()->removeAllDBConnections();
+ parent::tearDown();
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 1102ede13..a5807f451 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -42,6 +42,7 @@ public function test00() {
" migrations:ini: Create migrations tracking table.\n",
" migrations:dry-run: Preview pending migrations without executing.\n",
" migrations:status: Show migration status (applied and pending).\n",
+ " migrations:fresh: Rollback all migrations and run them fresh.\n",
], $this->executeMultiCommand([
'help',
]));
From 989cf6df71b2b3c50256b039a8ea19957e1c6cdd Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 10 Feb 2026 00:18:42 +0300
Subject: [PATCH 73/88] feat: Create Migration
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateMigrationCommand.php | 68 +++++++++++++
.../Writers/MigrationClassWriter.php | 72 ++++++++++++++
.../Tests/Cli/CreateMigrationCommandTest.php | 96 +++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
5 files changed, 238 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
create mode 100644 WebFiori/Framework/Writers/MigrationClassWriter.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 4adceaae4..1e54fd28f 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -362,6 +362,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\CreateTableCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateRepositoryCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateResourceCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateMigrationCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php b/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
new file mode 100644
index 000000000..309ec1cef
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
@@ -0,0 +1,68 @@
+getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter migration class name:', null, $validator);
+ } else if (empty(trim($className))) {
+ $this->error('--class-name cannot be empty string.');
+
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter migration class name:', null, $validator);
+ }
+
+ $className = trim($className);
+
+ $description = $this->getArgValue('--description');
+
+ if ($description === null && $this->getArgValue('--class-name') === null) {
+ // Only prompt if running interactively (no --class-name provided)
+ $description = $this->getInput('Enter migration description:', 'No description');
+ } else if ($description === null) {
+ $description = 'No description';
+ }
+
+ $writer = new MigrationClassWriter($className, trim($description));
+ $writer->writeClass();
+
+ $this->success('Migration class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/WebFiori/Framework/Writers/MigrationClassWriter.php b/WebFiori/Framework/Writers/MigrationClassWriter.php
new file mode 100644
index 000000000..bcfc4d7c7
--- /dev/null
+++ b/WebFiori/Framework/Writers/MigrationClassWriter.php
@@ -0,0 +1,72 @@
+description = $description;
+ $this->addUseStatement([
+ 'WebFiori\\Database\\Database',
+ 'WebFiori\\Database\\Schema\\AbstractMigration'
+ ]);
+ }
+
+ public function writeClassBody() {
+ $this->writeUpMethod();
+ $this->writeDownMethod();
+ $this->append('}');
+ }
+
+ public function writeClassComment() {
+ $this->append([
+ '/**',
+ ' * '.$this->description,
+ ' *',
+ ' * @author Ibrahim',
+ ' */'
+ ]);
+ }
+
+ public function writeClassDeclaration() {
+ $this->append('class '.$this->getName().' extends AbstractMigration {');
+ }
+
+ private function writeUpMethod() {
+ $this->append(' /**', 1);
+ $this->append(' * Apply the migration changes to the database.', 1);
+ $this->append(' *', 1);
+ $this->append(' * @param Database $db The database instance to execute changes on.', 1);
+ $this->append(' */', 1);
+ $this->append(' public function up(Database $db): void {', 1);
+ $this->append(' // TODO: Implement migration logic', 2);
+ $this->append(' }', 1);
+ $this->append('', 1);
+ }
+
+ private function writeDownMethod() {
+ $this->append(' /**', 1);
+ $this->append(' * Rollback the migration changes from the database.', 1);
+ $this->append(' *', 1);
+ $this->append(' * @param Database $db The database instance to execute rollback on.', 1);
+ $this->append(' */', 1);
+ $this->append(' public function down(Database $db): void {', 1);
+ $this->append(' // TODO: Implement rollback logic', 2);
+ $this->append(' }', 1);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php
new file mode 100644
index 000000000..02e919984
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php
@@ -0,0 +1,96 @@
+executeMultiCommand([
+ CreateMigrationCommand::class,
+ '--class-name' => $className,
+ '--description' => 'Test migration description'
+ ]);
+
+ $this->assertEquals([
+ "Success: Migration class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Migrations".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Database\\Migrations\\'.$className));
+ $this->removeClass('\\App\\Database\\Migrations\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateMigrationInteractive() {
+ $className = 'InteractiveMigration'.time();
+
+ $output = $this->executeSingleCommand(new CreateMigrationCommand(), [], [
+ $className,
+ 'Interactive migration description'
+ ]);
+
+ $this->assertEquals([
+ "Enter migration class name:\n",
+ "Enter migration description: Enter = 'No description'\n",
+ "Success: Migration class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Migrations".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Database\\Migrations\\'.$className));
+ $this->removeClass('\\App\\Database\\Migrations\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateMigrationWithEmptyClassName() {
+ $className = 'EmptyTestMigration'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateMigrationCommand::class,
+ '--class-name' => ''
+ ], [
+ $className
+ ]);
+
+ $this->assertEquals([
+ "Error: --class-name cannot be empty string.\n",
+ "Enter migration class name:\n",
+ "Success: Migration class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Migrations".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Database\\Migrations\\'.$className));
+ $this->removeClass('\\App\\Database\\Migrations\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateMigrationWithDefaultDescription() {
+ $className = 'DefaultDescMigration'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateMigrationCommand::class,
+ '--class-name' => $className
+ ]);
+
+ $this->assertEquals([
+ "Success: Migration class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Migrations".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Database\\Migrations\\'.$className));
+ $this->removeClass('\\App\\Database\\Migrations\\'.$className);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index a5807f451..05acb5a84 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -32,6 +32,7 @@ public function test00() {
" create:table: Create a new database table schema class.\n",
" create:repository: Create a new repository class.\n",
" create:resource: Create a complete CRUD resource (entity, table, repository, service).\n",
+ " create:migration: Create a new database migration class.\n",
From b3038d09b671a60957bece8c24c1bf25169a0c9f Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 10 Feb 2026 00:21:54 +0300
Subject: [PATCH 74/88] feat: Create Seeder
---
WebFiori/Framework/App.php | 1 +
.../Cli/Commands/CreateSeederCommand.php | 68 +++++++++++++
.../Framework/Writers/SeederClassWriter.php | 59 ++++++++++++
.../Tests/Cli/CreateSeederCommandTest.php | 96 +++++++++++++++++++
.../Framework/Tests/Cli/HelpCommandTest.php | 1 +
5 files changed, 225 insertions(+)
create mode 100644 WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
create mode 100644 WebFiori/Framework/Writers/SeederClassWriter.php
create mode 100644 tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 1e54fd28f..0962a778b 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -363,6 +363,7 @@ public static function getRunner() : Runner {
'\\WebFiori\\Framework\\Cli\\Commands\\CreateRepositoryCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateResourceCommand',
'\\WebFiori\\Framework\\Cli\\Commands\\CreateMigrationCommand',
+ '\\WebFiori\\Framework\\Cli\\Commands\\CreateSeederCommand',
diff --git a/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php b/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
new file mode 100644
index 000000000..e7206b724
--- /dev/null
+++ b/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
@@ -0,0 +1,68 @@
+getArgValue('--class-name');
+
+ if ($className === null) {
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter seeder class name:', null, $validator);
+ } else if (empty(trim($className))) {
+ $this->error('--class-name cannot be empty string.');
+
+ $validator = new InputValidator(function($input) {
+ return !empty(trim($input));
+ }, 'Class name cannot be empty.');
+
+ $className = $this->getInput('Enter seeder class name:', null, $validator);
+ }
+
+ $className = trim($className);
+
+ $description = $this->getArgValue('--description');
+
+ if ($description === null && $this->getArgValue('--class-name') === null) {
+ // Only prompt if running interactively (no --class-name provided)
+ $description = $this->getInput('Enter seeder description:', 'No description');
+ } else if ($description === null) {
+ $description = 'No description';
+ }
+
+ $writer = new SeederClassWriter($className, trim($description));
+ $writer->writeClass();
+
+ $this->success('Seeder class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+}
diff --git a/WebFiori/Framework/Writers/SeederClassWriter.php b/WebFiori/Framework/Writers/SeederClassWriter.php
new file mode 100644
index 000000000..3abefe72b
--- /dev/null
+++ b/WebFiori/Framework/Writers/SeederClassWriter.php
@@ -0,0 +1,59 @@
+description = $description;
+ $this->addUseStatement([
+ 'WebFiori\\Database\\Database',
+ 'WebFiori\\Database\\Schema\\AbstractSeeder'
+ ]);
+ }
+
+ public function writeClassBody() {
+ $this->writeRunMethod();
+ $this->append('}');
+ }
+
+ public function writeClassComment() {
+ $this->append([
+ '/**',
+ ' * '.$this->description,
+ ' *',
+ ' * @author Ibrahim',
+ ' */'
+ ]);
+ }
+
+ public function writeClassDeclaration() {
+ $this->append('class '.$this->getName().' extends AbstractSeeder {');
+ }
+
+ private function writeRunMethod() {
+ $this->append(' /**', 1);
+ $this->append(' * Run the seeder to populate the database with data.', 1);
+ $this->append(' *', 1);
+ $this->append(' * @param Database $db The database instance to execute seeding on.', 1);
+ $this->append(' */', 1);
+ $this->append(' public function run(Database $db): void {', 1);
+ $this->append(' // TODO: Implement seeding logic', 2);
+ $this->append(' }', 1);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php
new file mode 100644
index 000000000..96661dfb3
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php
@@ -0,0 +1,96 @@
+executeMultiCommand([
+ CreateSeederCommand::class,
+ '--class-name' => $className,
+ '--description' => 'Test seeder description'
+ ]);
+
+ $this->assertEquals([
+ "Success: Seeder class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Seeders".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Database\\Seeders\\'.$className));
+ $this->removeClass('\\App\\Database\\Seeders\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateSeederInteractive() {
+ $className = 'InteractiveSeeder'.time();
+
+ $output = $this->executeSingleCommand(new CreateSeederCommand(), [], [
+ $className,
+ 'Interactive seeder description'
+ ]);
+
+ $this->assertEquals([
+ "Enter seeder class name:\n",
+ "Enter seeder description: Enter = 'No description'\n",
+ "Success: Seeder class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Seeders".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Database\\Seeders\\'.$className));
+ $this->removeClass('\\App\\Database\\Seeders\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateSeederWithEmptyClassName() {
+ $className = 'EmptyTestSeeder'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateSeederCommand::class,
+ '--class-name' => ''
+ ], [
+ $className
+ ]);
+
+ $this->assertEquals([
+ "Error: --class-name cannot be empty string.\n",
+ "Enter seeder class name:\n",
+ "Success: Seeder class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Seeders".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Database\\Seeders\\'.$className));
+ $this->removeClass('\\App\\Database\\Seeders\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateSeederWithDefaultDescription() {
+ $className = 'DefaultDescSeeder'.time();
+
+ $output = $this->executeMultiCommand([
+ CreateSeederCommand::class,
+ '--class-name' => $className
+ ]);
+
+ $this->assertEquals([
+ "Success: Seeder class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Seeders".DIRECTORY_SEPARATOR.$className.".php\n"
+ ], $output);
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertTrue(class_exists('\\App\\Database\\Seeders\\'.$className));
+ $this->removeClass('\\App\\Database\\Seeders\\'.$className);
+ }
+}
diff --git a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
index 05acb5a84..e00b09b08 100644
--- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php
@@ -33,6 +33,7 @@ public function test00() {
" create:repository: Create a new repository class.\n",
" create:resource: Create a complete CRUD resource (entity, table, repository, service).\n",
" create:migration: Create a new database migration class.\n",
+ " create:seeder: Create a new database seeder class.\n",
From e3950b601a218abee6296bef73bb3adabd9165c6 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 10 Feb 2026 00:28:18 +0300
Subject: [PATCH 75/88] feat: Add Dependencies and Envs
---
.../Cli/Commands/CreateMigrationCommand.php | 142 +++++++++++++++++-
.../Cli/Commands/CreateSeederCommand.php | 142 +++++++++++++++++-
.../Writers/MigrationClassWriter.php | 55 ++++++-
.../Framework/Writers/SeederClassWriter.php | 55 ++++++-
.../Tests/Cli/CreateMigrationCommandTest.php | 6 +-
.../Tests/Cli/CreateSeederCommandTest.php | 6 +-
6 files changed, 386 insertions(+), 20 deletions(-)
diff --git a/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php b/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
index 309ec1cef..789e65058 100644
--- a/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
+++ b/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php
@@ -24,11 +24,27 @@ class CreateMigrationCommand extends Command {
public function __construct() {
parent::__construct('create:migration', [
new Argument('--class-name', 'The name of the migration class.', true),
- new Argument('--description', 'A description of what the migration does.', true)
+ new Argument('--description', 'A description of what the migration does.', true),
+ new Argument('--environments', 'Comma-separated list of environments (e.g., dev,test).', true),
+ new Argument('--depends-on', 'Comma-separated list of class names this depends on.', true)
], 'Create a new database migration class.');
}
public function exec(): int {
+ $className = $this->getClassName();
+ $description = $this->getMigrationDescription();
+ $environments = $this->getEnvironments();
+ $dependencies = $this->getDependencies();
+
+ $writer = new MigrationClassWriter($className, $description, $environments, $dependencies);
+ $writer->writeClass();
+
+ $this->success('Migration class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+
+ private function getClassName(): string {
$className = $this->getArgValue('--class-name');
if ($className === null) {
@@ -47,22 +63,132 @@ public function exec(): int {
$className = $this->getInput('Enter migration class name:', null, $validator);
}
- $className = trim($className);
-
+ return trim($className);
+ }
+
+ private function getMigrationDescription(): string {
$description = $this->getArgValue('--description');
if ($description === null && $this->getArgValue('--class-name') === null) {
- // Only prompt if running interactively (no --class-name provided)
$description = $this->getInput('Enter migration description:', 'No description');
} else if ($description === null) {
$description = 'No description';
}
- $writer = new MigrationClassWriter($className, trim($description));
- $writer->writeClass();
+ return trim($description);
+ }
+
+ private function getEnvironments(): array {
+ $environments = $this->getArgValue('--environments');
- $this->success('Migration class created at: '.$writer->getAbsolutePath());
+ if ($environments !== null) {
+ return array_map('trim', explode(',', $environments));
+ }
- return 0;
+ if ($this->getArgValue('--class-name') === null && $this->confirm('Restrict to specific environments?', false)) {
+ $envs = [];
+ while (true) {
+ $env = $this->getInput('Enter environment name (leave empty to finish):');
+ if (empty(trim($env))) {
+ break;
+ }
+ $envs[] = trim($env);
+ }
+ return $envs;
+ }
+
+ return [];
+ }
+
+ private function getDependencies(): array {
+ $dependsOn = $this->getArgValue('--depends-on');
+
+ if ($dependsOn !== null) {
+ return $this->resolveDependencies(array_map('trim', explode(',', $dependsOn)));
+ }
+
+ if ($this->getArgValue('--class-name') === null && $this->confirm('Add dependencies?', false)) {
+ return $this->selectDependenciesInteractive();
+ }
+
+ return [];
+ }
+
+ private function selectDependenciesInteractive(): array {
+ $available = $this->scanDatabaseChanges();
+
+ if (empty($available)) {
+ $this->info('No existing database changes found.');
+ return [];
+ }
+
+ $this->println('Available database changes:');
+ foreach ($available as $index => $class) {
+ $this->println(' '.($index + 1).'. '.$class);
+ }
+
+ $selected = [];
+ while (true) {
+ $input = $this->getInput('Select dependency (enter number, empty to finish):');
+ if (empty(trim($input))) {
+ break;
+ }
+
+ $index = (int)$input - 1;
+ if (isset($available[$index])) {
+ $selected[] = $available[$index];
+ $this->success('Added: '.$available[$index]);
+ } else {
+ $this->error('Invalid selection.');
+ }
+ }
+
+ return $selected;
+ }
+
+ private function scanDatabaseChanges(): array {
+ $changes = [];
+
+ // Scan migrations
+ $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
+ if (is_dir($migrationsPath)) {
+ foreach (glob($migrationsPath.DS.'*.php') as $file) {
+ $className = basename($file, '.php');
+ $changes[] = APP_DIR.'\\Database\\Migrations\\'.$className;
+ }
+ }
+
+ // Scan seeders
+ $seedersPath = APP_PATH.'Database'.DS.'Seeders';
+ if (is_dir($seedersPath)) {
+ foreach (glob($seedersPath.DS.'*.php') as $file) {
+ $className = basename($file, '.php');
+ $changes[] = APP_DIR.'\\Database\\Seeders\\'.$className;
+ }
+ }
+
+ return $changes;
+ }
+
+ private function resolveDependencies(array $shortNames): array {
+ $resolved = [];
+ $available = $this->scanDatabaseChanges();
+
+ foreach ($shortNames as $shortName) {
+ $found = false;
+ foreach ($available as $fullClass) {
+ if (basename(str_replace('\\', '/', $fullClass)) === $shortName) {
+ $resolved[] = $fullClass;
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ $this->warning("Dependency '$shortName' not found, skipping.");
+ }
+ }
+
+ return $resolved;
}
}
diff --git a/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php b/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
index e7206b724..9eef592db 100644
--- a/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
+++ b/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php
@@ -24,11 +24,27 @@ class CreateSeederCommand extends Command {
public function __construct() {
parent::__construct('create:seeder', [
new Argument('--class-name', 'The name of the seeder class.', true),
- new Argument('--description', 'A description of what the seeder does.', true)
+ new Argument('--description', 'A description of what the seeder does.', true),
+ new Argument('--environments', 'Comma-separated list of environments (e.g., dev,test).', true),
+ new Argument('--depends-on', 'Comma-separated list of class names this depends on.', true)
], 'Create a new database seeder class.');
}
public function exec(): int {
+ $className = $this->getClassName();
+ $description = $this->getSeederDescription();
+ $environments = $this->getEnvironments();
+ $dependencies = $this->getDependencies();
+
+ $writer = new SeederClassWriter($className, $description, $environments, $dependencies);
+ $writer->writeClass();
+
+ $this->success('Seeder class created at: '.$writer->getAbsolutePath());
+
+ return 0;
+ }
+
+ private function getClassName(): string {
$className = $this->getArgValue('--class-name');
if ($className === null) {
@@ -47,22 +63,132 @@ public function exec(): int {
$className = $this->getInput('Enter seeder class name:', null, $validator);
}
- $className = trim($className);
-
+ return trim($className);
+ }
+
+ private function getSeederDescription(): string {
$description = $this->getArgValue('--description');
if ($description === null && $this->getArgValue('--class-name') === null) {
- // Only prompt if running interactively (no --class-name provided)
$description = $this->getInput('Enter seeder description:', 'No description');
} else if ($description === null) {
$description = 'No description';
}
- $writer = new SeederClassWriter($className, trim($description));
- $writer->writeClass();
+ return trim($description);
+ }
+
+ private function getEnvironments(): array {
+ $environments = $this->getArgValue('--environments');
- $this->success('Seeder class created at: '.$writer->getAbsolutePath());
+ if ($environments !== null) {
+ return array_map('trim', explode(',', $environments));
+ }
- return 0;
+ if ($this->getArgValue('--class-name') === null && $this->confirm('Restrict to specific environments?', false)) {
+ $envs = [];
+ while (true) {
+ $env = $this->getInput('Enter environment name (leave empty to finish):');
+ if (empty(trim($env))) {
+ break;
+ }
+ $envs[] = trim($env);
+ }
+ return $envs;
+ }
+
+ return [];
+ }
+
+ private function getDependencies(): array {
+ $dependsOn = $this->getArgValue('--depends-on');
+
+ if ($dependsOn !== null) {
+ return $this->resolveDependencies(array_map('trim', explode(',', $dependsOn)));
+ }
+
+ if ($this->getArgValue('--class-name') === null && $this->confirm('Add dependencies?', false)) {
+ return $this->selectDependenciesInteractive();
+ }
+
+ return [];
+ }
+
+ private function selectDependenciesInteractive(): array {
+ $available = $this->scanDatabaseChanges();
+
+ if (empty($available)) {
+ $this->info('No existing database changes found.');
+ return [];
+ }
+
+ $this->println('Available database changes:');
+ foreach ($available as $index => $class) {
+ $this->println(' '.($index + 1).'. '.$class);
+ }
+
+ $selected = [];
+ while (true) {
+ $input = $this->getInput('Select dependency (enter number, empty to finish):');
+ if (empty(trim($input))) {
+ break;
+ }
+
+ $index = (int)$input - 1;
+ if (isset($available[$index])) {
+ $selected[] = $available[$index];
+ $this->success('Added: '.$available[$index]);
+ } else {
+ $this->error('Invalid selection.');
+ }
+ }
+
+ return $selected;
+ }
+
+ private function scanDatabaseChanges(): array {
+ $changes = [];
+
+ // Scan migrations
+ $migrationsPath = APP_PATH.'Database'.DS.'Migrations';
+ if (is_dir($migrationsPath)) {
+ foreach (glob($migrationsPath.DS.'*.php') as $file) {
+ $className = basename($file, '.php');
+ $changes[] = APP_DIR.'\\Database\\Migrations\\'.$className;
+ }
+ }
+
+ // Scan seeders
+ $seedersPath = APP_PATH.'Database'.DS.'Seeders';
+ if (is_dir($seedersPath)) {
+ foreach (glob($seedersPath.DS.'*.php') as $file) {
+ $className = basename($file, '.php');
+ $changes[] = APP_DIR.'\\Database\\Seeders\\'.$className;
+ }
+ }
+
+ return $changes;
+ }
+
+ private function resolveDependencies(array $shortNames): array {
+ $resolved = [];
+ $available = $this->scanDatabaseChanges();
+
+ foreach ($shortNames as $shortName) {
+ $found = false;
+ foreach ($available as $fullClass) {
+ if (basename(str_replace('\\', '/', $fullClass)) === $shortName) {
+ $resolved[] = $fullClass;
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ $this->warning("Dependency '$shortName' not found, skipping.");
+ }
+ }
+
+ return $resolved;
}
}
diff --git a/WebFiori/Framework/Writers/MigrationClassWriter.php b/WebFiori/Framework/Writers/MigrationClassWriter.php
index bcfc4d7c7..1376bc930 100644
--- a/WebFiori/Framework/Writers/MigrationClassWriter.php
+++ b/WebFiori/Framework/Writers/MigrationClassWriter.php
@@ -17,17 +17,33 @@
*/
class MigrationClassWriter extends ClassWriter {
private string $description;
+ private array $environments;
+ private array $dependencies;
- public function __construct(string $className, string $description = 'No description') {
+ public function __construct(string $className, string $description = 'No description', array $environments = [], array $dependencies = []) {
parent::__construct($className, APP_PATH.'Database'.DS.'Migrations', APP_DIR.'\\Database\\Migrations');
$this->description = $description;
+ $this->environments = $environments;
+ $this->dependencies = $dependencies;
$this->addUseStatement([
'WebFiori\\Database\\Database',
'WebFiori\\Database\\Schema\\AbstractMigration'
]);
+
+ foreach ($dependencies as $dep) {
+ $this->addUseStatement($dep);
+ }
}
public function writeClassBody() {
+ if (!empty($this->environments)) {
+ $this->writeGetEnvironments();
+ }
+
+ if (!empty($this->dependencies)) {
+ $this->writeGetDependencies();
+ }
+
$this->writeUpMethod();
$this->writeDownMethod();
$this->append('}');
@@ -47,6 +63,43 @@ public function writeClassDeclaration() {
$this->append('class '.$this->getName().' extends AbstractMigration {');
}
+ private function writeGetEnvironments() {
+ $this->append(' /**', 1);
+ $this->append(' * Get the environments where this migration should be executed.', 1);
+ $this->append(' *', 1);
+ $this->append(' * @return array Array of environment names.', 1);
+ $this->append(' */', 1);
+ $this->append(' public function getEnvironments(): array {', 1);
+ $this->append(' return [', 2);
+
+ foreach ($this->environments as $env) {
+ $this->append(" '$env',", 0);
+ }
+
+ $this->append(' ];', 2);
+ $this->append(' }', 1);
+ $this->append('', 1);
+ }
+
+ private function writeGetDependencies() {
+ $this->append(' /**', 1);
+ $this->append(' * Get the list of changes this migration depends on.', 1);
+ $this->append(' *', 1);
+ $this->append(' * @return array Array of class names.', 1);
+ $this->append(' */', 1);
+ $this->append(' public function getDependencies(): array {', 1);
+ $this->append(' return [', 2);
+
+ foreach ($this->dependencies as $dep) {
+ $shortName = basename(str_replace('\\', '/', $dep));
+ $this->append(" $shortName::class,", 0);
+ }
+
+ $this->append(' ];', 2);
+ $this->append(' }', 1);
+ $this->append('', 1);
+ }
+
private function writeUpMethod() {
$this->append(' /**', 1);
$this->append(' * Apply the migration changes to the database.', 1);
diff --git a/WebFiori/Framework/Writers/SeederClassWriter.php b/WebFiori/Framework/Writers/SeederClassWriter.php
index 3abefe72b..dc226f3e0 100644
--- a/WebFiori/Framework/Writers/SeederClassWriter.php
+++ b/WebFiori/Framework/Writers/SeederClassWriter.php
@@ -17,17 +17,33 @@
*/
class SeederClassWriter extends ClassWriter {
private string $description;
+ private array $environments;
+ private array $dependencies;
- public function __construct(string $className, string $description = 'No description') {
+ public function __construct(string $className, string $description = 'No description', array $environments = [], array $dependencies = []) {
parent::__construct($className, APP_PATH.'Database'.DS.'Seeders', APP_DIR.'\\Database\\Seeders');
$this->description = $description;
+ $this->environments = $environments;
+ $this->dependencies = $dependencies;
$this->addUseStatement([
'WebFiori\\Database\\Database',
'WebFiori\\Database\\Schema\\AbstractSeeder'
]);
+
+ foreach ($dependencies as $dep) {
+ $this->addUseStatement($dep);
+ }
}
public function writeClassBody() {
+ if (!empty($this->environments)) {
+ $this->writeGetEnvironments();
+ }
+
+ if (!empty($this->dependencies)) {
+ $this->writeGetDependencies();
+ }
+
$this->writeRunMethod();
$this->append('}');
}
@@ -46,6 +62,43 @@ public function writeClassDeclaration() {
$this->append('class '.$this->getName().' extends AbstractSeeder {');
}
+ private function writeGetEnvironments() {
+ $this->append(' /**', 1);
+ $this->append(' * Get the environments where this seeder should be executed.', 1);
+ $this->append(' *', 1);
+ $this->append(' * @return array Array of environment names.', 1);
+ $this->append(' */', 1);
+ $this->append(' public function getEnvironments(): array {', 1);
+ $this->append(' return [', 2);
+
+ foreach ($this->environments as $env) {
+ $this->append(" '$env',", 0);
+ }
+
+ $this->append(' ];', 2);
+ $this->append(' }', 1);
+ $this->append('', 1);
+ }
+
+ private function writeGetDependencies() {
+ $this->append(' /**', 1);
+ $this->append(' * Get the list of changes this seeder depends on.', 1);
+ $this->append(' *', 1);
+ $this->append(' * @return array Array of class names.', 1);
+ $this->append(' */', 1);
+ $this->append(' public function getDependencies(): array {', 1);
+ $this->append(' return [', 2);
+
+ foreach ($this->dependencies as $dep) {
+ $shortName = basename(str_replace('\\', '/', $dep));
+ $this->append(" $shortName::class,", 0);
+ }
+
+ $this->append(' ];', 2);
+ $this->append(' }', 1);
+ $this->append('', 1);
+ }
+
private function writeRunMethod() {
$this->append(' /**', 1);
$this->append(' * Run the seeder to populate the database with data.', 1);
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php
index 02e919984..70d9de0fb 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php
@@ -39,12 +39,16 @@ public function testCreateMigrationInteractive() {
$output = $this->executeSingleCommand(new CreateMigrationCommand(), [], [
$className,
- 'Interactive migration description'
+ 'Interactive migration description',
+ 'n', // No environments
+ 'n' // No dependencies
]);
$this->assertEquals([
"Enter migration class name:\n",
"Enter migration description: Enter = 'No description'\n",
+ "Restrict to specific environments?(y/N)\n",
+ "Add dependencies?(y/N)\n",
"Success: Migration class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Migrations".DIRECTORY_SEPARATOR.$className.".php\n"
], $output);
$this->assertEquals(0, $this->getExitCode());
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php
index 96661dfb3..a4dd92ce7 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php
@@ -39,12 +39,16 @@ public function testCreateSeederInteractive() {
$output = $this->executeSingleCommand(new CreateSeederCommand(), [], [
$className,
- 'Interactive seeder description'
+ 'Interactive seeder description',
+ 'n', // No environments
+ 'n' // No dependencies
]);
$this->assertEquals([
"Enter seeder class name:\n",
"Enter seeder description: Enter = 'No description'\n",
+ "Restrict to specific environments?(y/N)\n",
+ "Add dependencies?(y/N)\n",
"Success: Seeder class created at: ".APP_PATH."Database".DIRECTORY_SEPARATOR."Seeders".DIRECTORY_SEPARATOR.$className.".php\n"
], $output);
$this->assertEquals(0, $this->getExitCode());
From 67d8a9fee6165d3ddce874014f5281cf7c8ebe6b Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 11 Feb 2026 23:27:08 +0300
Subject: [PATCH 76/88] test: Updated Test Cases
---
.../Tests/Cli/AddDbConnectionCommandTest.php | 33 ++++---
.../Tests/Cli/CreateTableCommandTest.php | 99 +++++++++++++++++++
.../Tests/Cli/DryRunMigrationsCommandTest.php | 16 +++
3 files changed, 133 insertions(+), 15 deletions(-)
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
index 78f68f6aa..d6e55a34c 100644
--- a/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php
@@ -35,6 +35,7 @@ public function testAddDBConnection00() {
"Port number: Enter = '3306'\n",
"Username:\n",
"Password:\n",
+ "******\n",
"Database name:\n",
"Give your connection a friendly name: Enter = '$connName'\n",
"Trying to connect to the database...\n",
@@ -73,14 +74,15 @@ public function testAddDBConnection01() {
$this->assertEquals("Port number: Enter = '3306'\n", $output[4]);
$this->assertEquals("Username:\n", $output[5]);
$this->assertEquals("Password:\n", $output[6]);
- $this->assertEquals("Database name:\n", $output[7]);
- $this->assertEquals("Give your connection a friendly name: Enter = '$connName'\n", $output[8]);
- $this->assertEquals("Trying to connect to the database...\n", $output[9]);
- $this->assertEquals("Trying with 'localhost'...\n", $output[10]);
- $this->assertEquals("Error: Unable to connect to the database.\n", $output[11]);
- $this->assertStringContainsString("Error: Unable to connect to database: 1045 - Access denied for user", $output[12]);
- $this->assertEquals("Would you like to store connection information anyway?(y/N)\n", $output[13]);
- $this->assertEquals("Success: Connection information was stored in application configuration.\n", $output[14]);
+ $this->assertEquals("***********\n", $output[7]);
+ $this->assertEquals("Database name:\n", $output[8]);
+ $this->assertEquals("Give your connection a friendly name: Enter = '$connName'\n", $output[9]);
+ $this->assertEquals("Trying to connect to the database...\n", $output[10]);
+ $this->assertEquals("Trying with 'localhost'...\n", $output[11]);
+ $this->assertEquals("Error: Unable to connect to the database.\n", $output[12]);
+ $this->assertStringContainsString("Error: Unable to connect to database: 1045 - Access denied for user", $output[13]);
+ $this->assertEquals("Would you like to store connection information anyway?(y/N)\n", $output[14]);
+ $this->assertEquals("Success: Connection information was stored in application configuration.\n", $output[15]);
}
/**
* @test
@@ -113,12 +115,13 @@ public function testAddDBConnection02() {
$this->assertEquals("Port number: Enter = '3306'\n", $output[4]);
$this->assertEquals("Username:\n", $output[5]);
$this->assertEquals("Password:\n", $output[6]);
- $this->assertEquals("Database name:\n", $output[7]);
- $this->assertEquals("Give your connection a friendly name: Enter = '$connName'\n", $output[8]);
- $this->assertEquals("Trying to connect to the database...\n", $output[9]);
- $this->assertEquals("Trying with 'localhost'...\n", $output[10]);
- $this->assertEquals("Error: Unable to connect to the database.\n", $output[11]);
- $this->assertStringContainsString("Error: Unable to connect to database: 1045 - Access denied for user", $output[12]);
- $this->assertEquals("Would you like to store connection information anyway?(y/N)\n", $output[13]);
+ $this->assertEquals("***********\n", $output[7]);
+ $this->assertEquals("Database name:\n", $output[8]);
+ $this->assertEquals("Give your connection a friendly name: Enter = '$connName'\n", $output[9]);
+ $this->assertEquals("Trying to connect to the database...\n", $output[10]);
+ $this->assertEquals("Trying with 'localhost'...\n", $output[11]);
+ $this->assertEquals("Error: Unable to connect to the database.\n", $output[12]);
+ $this->assertStringContainsString("Error: Unable to connect to database: 1045 - Access denied for user", $output[13]);
+ $this->assertEquals("Would you like to store connection information anyway?(y/N)\n", $output[14]);
}
}
diff --git a/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
index 7c74b4540..4075b30f8 100644
--- a/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php
@@ -130,4 +130,103 @@ public function testCreateTableWithArgs03() {
$this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
$this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
}
+
+ /**
+ * @test
+ */
+ public function testCreateTableWithMultipleColumnsViaJson() {
+ $className = 'TestTable'.time();
+ $columnsJson = json_encode([
+ ['name' => 'id', 'type' => 'INT', 'size' => 11, 'primary' => true, 'autoIncrement' => true],
+ ['name' => 'name', 'type' => 'VARCHAR', 'size' => 255, 'nullable' => false],
+ ['name' => 'email', 'type' => 'VARCHAR', 'size' => 255, 'nullable' => true],
+ ['name' => 'created_at', 'type' => 'DATETIME', 'nullable' => false]
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateTableCommand::class,
+ '--class-name' => $className,
+ '--table-name' => 'users',
+ '--columns' => $columnsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertStringContainsString("Success: Table class created", implode('', $output));
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateTableWithNullableColumn() {
+ $className = 'TestTable'.time();
+ $columnsJson = json_encode([
+ ['name' => 'description', 'type' => 'TEXT', 'nullable' => true]
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateTableCommand::class,
+ '--class-name' => $className,
+ '--table-name' => 'items',
+ '--columns' => $columnsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertStringContainsString("Success: Table class created", implode('', $output));
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateTableWithPrimaryKeyNoAutoIncrement() {
+ $className = 'TestTable'.time();
+ $columnsJson = json_encode([
+ ['name' => 'uuid', 'type' => 'VARCHAR', 'size' => 36, 'primary' => true, 'nullable' => false]
+ ]);
+
+ $output = $this->executeMultiCommand([
+ CreateTableCommand::class,
+ '--class-name' => $className,
+ '--table-name' => 'entities',
+ '--columns' => $columnsJson
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $this->assertStringContainsString("Success: Table class created", implode('', $output));
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
+
+ /**
+ * @test
+ */
+ public function testCreateTableWithInteractiveColumnAddition() {
+ $className = 'TestTable'.time();
+
+ $output = $this->executeSingleCommand(new CreateTableCommand(), [], [
+ $className,
+ 'users',
+ 'y', // Add columns
+ 'id', // Column name
+ '11', // INT type (index 11 in getSupportedDataTypes)
+ '11', // Size
+ 'y', // Primary key
+ 'y', // Auto increment
+ 'n', // Not nullable
+ '' // Finish adding columns
+ ]);
+
+ $this->assertEquals(0, $this->getExitCode());
+ $outputStr = implode('', $output);
+ $this->assertStringContainsString('Enter table class name:', $outputStr);
+ $this->assertStringContainsString('Add columns to the table?', $outputStr);
+ $this->assertStringContainsString('Enter column name', $outputStr);
+ $this->assertStringContainsString('Select column type:', $outputStr);
+ $this->assertStringContainsString('Success: Table class created', $outputStr);
+ $this->assertTrue(class_exists('\\App\\Infrastructure\\Schema\\'.$className));
+ $this->removeClass('\\App\\Infrastructure\\Schema\\'.$className);
+ }
}
diff --git a/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
index d702f8dd5..0b1ab158c 100644
--- a/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php
@@ -65,6 +65,7 @@ public function testDryRunWithNoMigrations() {
*/
public function testDryRunWithPendingMigration() {
$this->createTestMigration('TestMigration');
+ $this->initMigrations();
$output = $this->executeMultiCommand([
DryRunMigrationsCommand::class,
@@ -82,6 +83,7 @@ public function testDryRunWithPendingMigration() {
*/
public function testDryRunWithCustomEnv() {
$this->createTestMigration('EnvTestMigration');
+ $this->initMigrations('staging');
$output = $this->executeMultiCommand([
DryRunMigrationsCommand::class,
@@ -100,6 +102,7 @@ public function testDryRunWithCustomEnv() {
*/
public function testDryRunShowsQueries() {
$this->createTestMigrationWithSchema('QueryTestMigration');
+ $this->initMigrations();
$output = $this->executeMultiCommand([
DryRunMigrationsCommand::class,
@@ -114,6 +117,19 @@ public function testDryRunShowsQueries() {
$this->assertEquals(0, $this->getExitCode());
}
+ private function initMigrations(string $env = 'dev'): void {
+ $args = [
+ 'WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand',
+ '--connection' => 'test-connection'
+ ];
+
+ if ($env !== 'dev') {
+ $args['--env'] = $env;
+ }
+
+ $this->executeMultiCommand($args);
+ }
+
private function createTestMigrationWithSchema(string $name): void {
$dir = APP_PATH.'Database'.DS.'Migrations';
From 21836436e84485ac62b57dee5bee324655593473 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 11 Feb 2026 23:27:45 +0300
Subject: [PATCH 77/88] refactor: Masked Password For DB
---
WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php b/WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php
index 99cf432f5..2f4884725 100644
--- a/WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php
+++ b/WebFiori/Framework/Cli/Commands/AddDbConnectionCommand.php
@@ -43,7 +43,7 @@ public function exec() : int {
$connInfoObj->setHost($this->getInput('Database host:', '127.0.0.1'));
$connInfoObj->setPort($this->getInput('Port number:', 3306));
$connInfoObj->setUsername($this->getInput('Username:'));
- $connInfoObj->setPassword($this->getInput('Password:'));
+ $connInfoObj->setPassword($this->getMaskedInput('Password:'));
$connInfoObj->setDBName($this->getInput('Database name:'));
$connInfoObj->setName($this->getInput('Give your connection a friendly name:', 'db-connection-'.(count(App::getConfig()->getDBConnections()) + 1)));
$this->println('Trying to connect to the database...');
From 240e0dd7ffe7b8e4291cb7b31489b658fc7e9587 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 11 Feb 2026 23:28:17 +0300
Subject: [PATCH 78/88] refactor: use of `getSupportedDataTypes`
---
WebFiori/Framework/Cli/Commands/CreateTableCommand.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/WebFiori/Framework/Cli/Commands/CreateTableCommand.php b/WebFiori/Framework/Cli/Commands/CreateTableCommand.php
index 017508b8d..6a7a593d5 100644
--- a/WebFiori/Framework/Cli/Commands/CreateTableCommand.php
+++ b/WebFiori/Framework/Cli/Commands/CreateTableCommand.php
@@ -65,7 +65,7 @@ private function getTableColumns() : array {
break;
}
- $colType = $this->select('Select column type:', DataType::getTypes());
+ $colType = $this->select('Select column type:', DataType::getSupportedDataTypes('mysql'));
$column = [
'name' => trim($colName),
From 1d6340c93cffc8b90ab3d3b99179ceddb335a355 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 11 Feb 2026 23:33:36 +0300
Subject: [PATCH 79/88] feat: Added Options to `AttributeTableWriter`
---
.../Framework/Writers/AttributeTableWriter.php | 16 ++++++++++++++--
1 file changed, 14 insertions(+), 2 deletions(-)
diff --git a/WebFiori/Framework/Writers/AttributeTableWriter.php b/WebFiori/Framework/Writers/AttributeTableWriter.php
index 129518884..e14ed4b7c 100644
--- a/WebFiori/Framework/Writers/AttributeTableWriter.php
+++ b/WebFiori/Framework/Writers/AttributeTableWriter.php
@@ -55,7 +55,8 @@ public function writeClassComment() {
// Add Column attributes
foreach ($this->columns as $col) {
- $attr = "#[Column(name: '{$col['name']}', type: DataType::{$col['type']}";
+ $type = strtoupper($col['type']);
+ $attr = "#[Column(name: '{$col['name']}', type: DataType::{$type}";
if (isset($col['size'])) {
$attr .= ", size: {$col['size']}";
@@ -69,7 +70,18 @@ public function writeClassComment() {
if (isset($col['nullable']) && $col['nullable']) {
$attr .= ", nullable: true";
}
-
+ if (isset($col['identity']) && $col['identity']) {
+ $attr .= ", identity: true";
+ }
+ if (isset($col['autoUpdate']) && $col['autoUpdate']) {
+ $attr .= ", autoUpdate: true";
+ }
+ if (isset($col['default'])) {
+ $attr .= ", default: ".$col['default'];
+ }
+ if (isset($col['comment']) && strlen($col['comment']) != 0) {
+ $attr .= ", comment: ".$col['comment'];
+ }
$attr .= ')]';
$this->append($attr, 0);
}
From 12a88a90abdb4e27b3dfbb4784b96ab6a3e637cb Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Thu, 12 Feb 2026 00:16:31 +0300
Subject: [PATCH 80/88] refactor: Use of Masked Input
---
WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php | 2 +-
.../Framework/Tests/Cli/AddSmtpConnectionCommandTest.php | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php b/WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php
index 3fbb68ee5..c5d88aac6 100644
--- a/WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php
+++ b/WebFiori/Framework/Cli/Commands/AddSmtpConnectionCommand.php
@@ -47,7 +47,7 @@ public function exec() : int {
}
$smtpConn->setPort($this->getInput('Port number:', $smtpConn->getPort()));
$smtpConn->setUsername($this->getInput('Username:'));
- $smtpConn->setPassword($this->getInput('Password:'));
+ $smtpConn->setPassword($this->getMaskedInput('Password:'));
$smtpConn->setAddress($this->getInput('Sender email address:', $smtpConn->getUsername()));
$smtpConn->setSenderName($this->getInput('Sender name:', $smtpConn->getAddress()));
$smtpConn->setAccountName($this->getInput('Give your connection a friendly name:', 'smtp-connection-'.count(App::getConfig()->getSMTPConnections())));
diff --git a/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
index 1b8784fde..175327718 100644
--- a/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
+++ b/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php
@@ -37,6 +37,7 @@ public function testAddSMTPConnection00() {
"Port number: Enter = '25'\n",
"Username:\n",
"Password:\n",
+ "********\n",
"Sender email address: Enter = 'test@example.com'\n",
"Sender name: Enter = 'test@example.com'\n",
"Give your connection a friendly name: Enter = '$connName'\n",
From f28f9776e7b19d91b352ef1e3fb4a1cc7a793674 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Tue, 17 Feb 2026 23:41:30 +0300
Subject: [PATCH 81/88] feat: Env Vars for Json Driver
---
WebFiori/Framework/Config/Controller.php | 25 ++++++++++++
WebFiori/Framework/Config/JsonDriver.php | 40 ++++++++++++-------
.../Framework/Tests/Config/JsonDriverTest.php | 34 ++++++++++++++++
3 files changed, 85 insertions(+), 14 deletions(-)
diff --git a/WebFiori/Framework/Config/Controller.php b/WebFiori/Framework/Config/Controller.php
index 367a44b5d..fc04303d6 100644
--- a/WebFiori/Framework/Config/Controller.php
+++ b/WebFiori/Framework/Config/Controller.php
@@ -109,6 +109,25 @@ public static function setDriver(ConfigurationDriver $driver) {
self::get()->driver = $driver;
self::init($driver);
}
+ /**
+ * Resolves environment variable references in configuration values.
+ *
+ * If value starts with 'env:', attempts to read from environment.
+ * Falls back to original value if env var doesn't exist.
+ *
+ * @param mixed $value The value to resolve
+ *
+ * @return mixed The resolved value
+ */
+ public static function resolveEnvValue(string $value) {
+
+ if (str_starts_with($value, 'env:')) {
+ $envVar = substr($value, 4);
+
+ return getenv($envVar) ?: ($_ENV[$envVar] ?? $value);
+ }
+ return $value;
+ }
/**
* Reads application environment variables and updates the class which holds
* application environment variables.
@@ -117,6 +136,12 @@ public static function setDriver(ConfigurationDriver $driver) {
*/
public static function updateEnv() {
foreach (self::getDriver()->getEnvVars() as $name => $envVar) {
+ if (is_string($envVar) && str_starts_with($envVar, 'env:')) {
+ $envVar = self::resolveEnvValue($envVar);
+ } else if (is_array($envVar) && isset($envVar['value']) && str_starts_with($envVar['value'], 'env:')) {
+ $envVar['value'] = self::resolveEnvValue($envVar['value']);
+ }
+
if (!defined($name)) {
if (isset($envVar['value'])) {
define($name, $envVar['value']);
diff --git a/WebFiori/Framework/Config/JsonDriver.php b/WebFiori/Framework/Config/JsonDriver.php
index 831a0594d..487882c9a 100644
--- a/WebFiori/Framework/Config/JsonDriver.php
+++ b/WebFiori/Framework/Config/JsonDriver.php
@@ -117,7 +117,7 @@ public function addOrUpdateSMTPAccount(SMTPAccount $emailAccount) {
'password' => $emailAccount->getPassword(),
'address' => $emailAccount->getAddress(),
'sender-name' => $emailAccount->getSenderName(),
-
+ 'access-token' => $emailAccount->getAccessToken()
], 'none', 'same');
$this->json->get('smtp-connections')->add($emailAccount->getAccountName(), $connectionAsJson);
$this->writeJson();
@@ -206,12 +206,12 @@ public function getDBConnection(string $conName) {
}
return new ConnectionInfo(
- $jsonObj->get('type'),
- $jsonObj->get('username'),
- $jsonObj->get('password'),
- $jsonObj->get('database'),
- $jsonObj->get('host'),
- $jsonObj->get('port'),
+ $this->getProp($jsonObj, 'type', $conName),
+ $this->getProp($jsonObj, 'username', $conName),
+ $this->getProp($jsonObj, 'password', $conName),
+ $this->getProp($jsonObj, 'database', $conName),
+ $this->getProp($jsonObj, 'host', $conName),
+ $this->getProp($jsonObj, 'port', $conName),
$extrasArr);
}
}
@@ -291,9 +291,19 @@ public function getEnvVars(): array {
$vars = $this->json->get('env-vars');
foreach ($vars->getPropsNames() as $name) {
+ $value = '';
+ $desc = null;
+ $val = $this->json->get('env-vars')->get($name);
+
+ if (gettype($val) == 'object') {
+ $value = $val->get('value');
+ $desc = $val->get('description');
+ } else {
+ $value = $val;
+ }
$retVal[$name] = [
- 'value' => $this->json->get('env-vars')->get($name)->get('value'),
- 'description' => $this->json->get('env-vars')->get($name)->get('description')
+ 'value' => Controller::resolveEnvValue($value),
+ 'description' => $desc
];
}
@@ -332,13 +342,13 @@ public function getPrimaryLanguage(): string {
* password.
*/
public function getSchedulerPassword(): string {
- $pass = $this->json->get('scheduler-password') ?? 'NO_PASSWORD';
+ $pass = ''.$this->json->get('scheduler-password') ?? 'NO_PASSWORD';
- if (strlen($pass.'') == 0 || $pass == 'NO_PASSWORD') {
+ if (strlen($pass) == 0 || $pass == 'NO_PASSWORD') {
return 'NO_PASSWORD';
}
- return $pass;
+ return Controller::resolveEnvValue($pass);
}
/**
* Returns SMTP connection given its name.
@@ -365,7 +375,9 @@ public function getSMTPConnection(string $name) {
'sender-name' => $this->getProp($jsonObj, 'sender-name', $name),
'server-address' => $this->getProp($jsonObj, 'host', $name),
'user' => $this->getProp($jsonObj, 'username', $name),
- 'account-name' => $name
+ 'user' => $this->getProp($jsonObj, 'username', $name),
+ 'account-name' => $name,
+ 'access-token' => $this->getProp($jsonObj, 'access-token', $name)
]);
}
}
@@ -735,7 +747,7 @@ private function getProp(Json $j, $name, string $connName) {
throw new InitializationException('The property "'.$name.'" of the connection "'.$connName.'" is missing.');
}
- return $val;
+ return Controller::resolveEnvValue($val);
}
private function isValidLangCode($langCode) {
$code = strtoupper(trim($langCode));
diff --git a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
index e714dbb02..eb658c56e 100644
--- a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
+++ b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
@@ -628,4 +628,38 @@ public function testSMTPConnections04() {
$account = $driver->getSMTPConnection('Cool2');
$this->assertNotNull($account);
}
+ public function testEnvVars00() {
+ $driver = new JsonDriver();
+ $driver->setConfigFileName('config-with-env.json');
+ putenv('HOST=22.22.22.22');
+ putenv('VERBOSE=1');
+ putenv('SMTP_00_USER=test@example.com');
+ putenv('SMTP_00_PASS=3241');
+ putenv('SMTP_00_ADDRESS=test2@example.com');
+ putenv('SMTP_00_NAME=Ibrahim');
+ putenv('SMTP_TOKEN=some_tkn');
+
+ putenv('DB_HOST_2=122.76.76.87');
+ putenv('DB_NAME_2=Ibrahim');
+ putenv('DB_PASS_2=some_pass');
+
+
+ $driver->initialize();
+ $vars = $driver->getEnvVars();
+
+ $this->assertEquals('22.22.22.22', $vars['HOST']['value']);
+ $this->assertEquals('1', $vars['WF_VERBOSE_2']['value']);
+
+ $smtp = $driver->getSMTPConnection('conn00');
+ $this->assertEquals('test2@example.com', $smtp->getAddress());
+ $this->assertEquals('some_tkn', $smtp->getAccessToken());
+ $this->assertEquals('3241', $smtp->getPassword());
+ $this->assertEquals('test@example.com', $smtp->getUsername());
+ $this->assertEquals('Ibrahim', $smtp->getSenderName());
+
+ $dbConn00 = $driver->getDBConnection('New_Connection_2');
+ $this->assertEquals('122.76.76.87', $dbConn00->getHost());
+ $this->assertEquals('Ibrahim', $dbConn00->getDBName());
+ $this->assertEquals('some_pass', $dbConn00->getPassword());
+ }
}
From e9ad0f40a4cc211a00e4d6881d963c1e2af47fd9 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 18 Feb 2026 00:06:13 +0300
Subject: [PATCH 82/88] test: Added Env Vars Test Config
---
App/Config/config-with-env.json | 69 +++++++++++++++++++++++++++++++++
1 file changed, 69 insertions(+)
create mode 100644 App/Config/config-with-env.json
diff --git a/App/Config/config-with-env.json b/App/Config/config-with-env.json
new file mode 100644
index 000000000..f4624fd38
--- /dev/null
+++ b/App/Config/config-with-env.json
@@ -0,0 +1,69 @@
+{
+ "base-url":"DYNAMIC",
+ "theme":null,
+ "home-page":"BASE_URL",
+ "primary-lang":"env:LANG",
+ "titles":{
+ "AR":"افتراضي",
+ "EN":"Default"
+ },
+ "name-separator":"|",
+ "scheduler-password":"env:SCHEDULER_PASS",
+ "app-names":{
+ "AR":"تطبيق",
+ "EN":"Application"
+ },
+ "app-descriptions":{
+ "AR":"",
+ "EN":""
+ },
+ "version-info":{
+ "version":"1.0",
+ "version-type":"Stable",
+ "release-date":"2023-10-30"
+ },
+ "env-vars":{
+ "WF_VERBOSE_2":{
+ "value":"env:VERBOSE",
+ "description":"Configure the verbosity of error messsages at run-time. This should be set to true in testing and false in production."
+ },
+ "CLI_HTTP_HOST_2":{
+ "value":"example.com",
+ "description":"Host name that will be used when runing the application as command line utility."
+ },
+ "HOST":"env:HOST"
+ },
+ "smtp-connections":{
+ "conn00": {
+ "host":"smtp.outlook.com",
+ "port":"244",
+ "username":"env:SMTP_00_USER",
+ "password":"env:SMTP_00_PASS",
+ "address":"env:SMTP_00_ADDRESS",
+ "sender-name":"env:SMTP_00_NAME",
+ "access-token":"env:SMTP_TOKEN"
+ }
+ },
+ "database-connections":{
+ "New_Connection":{
+ "type":"mysql",
+ "host":"env:DB_HOST_1",
+ "port":3306,
+ "username":"cool",
+ "database":"env:DB_NAME_1",
+ "password":"env:DB_PASS_1",
+ "extras":{
+ }
+ },
+ "New_Connection_2":{
+ "type":"mysql",
+ "host":"env:DB_HOST_2",
+ "port":3306,
+ "database":"env:DB_NAME_2",
+ "password":"env:DB_PASS_2",
+ "username":"cool",
+ "extras":{
+ }
+ }
+ }
+}
\ No newline at end of file
From f0ee1a715a3cc1cb4920ad3c779a87f1813748f3 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 18 Feb 2026 00:06:47 +0300
Subject: [PATCH 83/88] test: Added Scheduler Password Test
---
tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
index eb658c56e..4919d2ba6 100644
--- a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
+++ b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
@@ -661,5 +661,8 @@ public function testEnvVars00() {
$this->assertEquals('122.76.76.87', $dbConn00->getHost());
$this->assertEquals('Ibrahim', $dbConn00->getDBName());
$this->assertEquals('some_pass', $dbConn00->getPassword());
+
+ putenv('SCHEDULER_PASS=my_secure_hash');
+ $this->assertEquals('my_secure_hash', $driver->getSchedulerPassword());
}
}
From b34cd182ae731d9b814b3362da1332b103e08277 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 18 Feb 2026 00:07:18 +0300
Subject: [PATCH 84/88] feat: Optional Config Var
---
WebFiori/Framework/Config/JsonDriver.php | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/WebFiori/Framework/Config/JsonDriver.php b/WebFiori/Framework/Config/JsonDriver.php
index 487882c9a..440c907fb 100644
--- a/WebFiori/Framework/Config/JsonDriver.php
+++ b/WebFiori/Framework/Config/JsonDriver.php
@@ -375,9 +375,8 @@ public function getSMTPConnection(string $name) {
'sender-name' => $this->getProp($jsonObj, 'sender-name', $name),
'server-address' => $this->getProp($jsonObj, 'host', $name),
'user' => $this->getProp($jsonObj, 'username', $name),
- 'user' => $this->getProp($jsonObj, 'username', $name),
'account-name' => $name,
- 'access-token' => $this->getProp($jsonObj, 'access-token', $name)
+ 'access-token' => $this->getProp($jsonObj, 'access-token', $name, false)
]);
}
}
@@ -402,6 +401,7 @@ public function getSMTPConnections(): array {
$acc->setSenderName($this->getProp($jsonObj, 'sender-name', $name));
$acc->setServerAddress($this->getProp($jsonObj, 'host', $name));
$acc->setUsername($this->getProp($jsonObj, 'username', $name));
+ $acc->setAccessToken($this->getProp($jsonObj, 'access-token', $name, false));
$retVal[$name] = $acc;
}
@@ -740,10 +740,10 @@ public function setTitleSeparator(string $separator) {
public function toJSON() : Json {
return $this->json;
}
- private function getProp(Json $j, $name, string $connName) {
+ private function getProp(Json $j, $name, string $connName, bool $requred = true) {
$val = $j->get($name);
- if ($val === null) {
+ if ($val === null && $requred) {
throw new InitializationException('The property "'.$name.'" of the connection "'.$connName.'" is missing.');
}
From 32da35f1252461aa659392ec75b68efae1122370 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 18 Feb 2026 01:06:49 +0300
Subject: [PATCH 85/88] docs: Updated PHP Docs For Config
---
.../Framework/Config/ConfigurationDriver.php | 28 +++++++++++
WebFiori/Framework/Config/Controller.php | 34 +++++++++++--
WebFiori/Framework/Config/JsonDriver.php | 50 +++++++++++++++++--
3 files changed, 104 insertions(+), 8 deletions(-)
diff --git a/WebFiori/Framework/Config/ConfigurationDriver.php b/WebFiori/Framework/Config/ConfigurationDriver.php
index 93382a08b..9c68f8229 100644
--- a/WebFiori/Framework/Config/ConfigurationDriver.php
+++ b/WebFiori/Framework/Config/ConfigurationDriver.php
@@ -20,6 +20,9 @@ interface ConfigurationDriver {
* the constant will be accesaable anywhere within the application's environment.
* Additionally, it will be added as environment variable using 'putenv()'.
*
+ * Note: The value parameter supports the 'env:' prefix for referencing
+ * system environment variables (e.g., "env:MY_VAR").
+ *
* @param string $name The name of the named constant such as 'MY_CONSTANT'.
*
* @param mixed|null $value The value of the constant.
@@ -31,6 +34,9 @@ public function addEnvVar(string $name, mixed $value = null, ?string $descriptio
/**
* Adds new database connections information or update existing connections.
*
+ * Note: When using JsonDriver, connection properties support environment variable
+ * substitution using the 'env:' prefix. For example, you can set host to "env:DB_HOST"
+ * in the JSON configuration file, and it will be resolved at runtime.
*
* @param ConnectionInfo $dbConnectionsInfo An object which holds connection information.
*/
@@ -38,6 +44,10 @@ public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo);
/**
* Adds new SMTP account or Updates an existing one.
*
+ * Note: When using JsonDriver, SMTP account properties support environment variable
+ * substitution using the 'env:' prefix. For example, you can set password to "env:SMTP_PASS"
+ * in the JSON configuration file, and it will be resolved at runtime.
+ *
* @param SMTPAccount $emailAccount An instance of 'SMTPAccount'.
*/
public function addOrUpdateSMTPAccount(SMTPAccount $emailAccount);
@@ -92,6 +102,9 @@ public function getBaseURL() : string;
/**
* Returns database connection information given connection name.
*
+ * Note: Connection properties that use the 'env:' prefix in configuration
+ * will be automatically resolved to their environment variable values.
+ *
* @param string $conName The name of the connection.
*
* @return ConnectionInfo|null The method should return an object of type
@@ -107,6 +120,9 @@ public function getDBConnection(string $conName);
* The keys of the array should be the name of database connection and the
* value of each key should be an object of type ConnectionInfo.
*
+ * Note: Connection properties that use the 'env:' prefix in configuration
+ * will be automatically resolved to their environment variable values.
+ *
* @return array An associative array.
*/
public function getDBConnections() : array;
@@ -133,6 +149,9 @@ public function getDescriptions() : array;
/**
* Returns an associative array of application constants.
*
+ * Note: Environment variable values that use the 'env:' prefix in configuration
+ * will be automatically resolved to their system environment variable values.
+ *
* @return array The indices of the array are names of the constants and
* values are sub-associative arrays. Each sub-array must have two indices,
* 'value' and 'description'.
@@ -158,6 +177,9 @@ public function getPrimaryLanguage() : string;
* return the hashed value. If no password is set, this method should return the
* string 'NO_PASSWORD'.
*
+ * Note: The scheduler password value supports environment variable substitution
+ * using the 'env:' prefix (e.g., "env:SCHEDULER_PASS" in configuration).
+ *
* @return string Password hash or the string 'NO_PASSWORD' if there is no
* password.
*/
@@ -165,6 +187,9 @@ public function getSchedulerPassword() : string;
/**
* Returns SMTP connection given its name.
*
+ * Note: SMTP account properties that use the 'env:' prefix in configuration
+ * will be automatically resolved to their environment variable values.
+ *
* The method should be implemented in a way that it searches
* for an account with the given name in the set
* of added accounts. If no account was found, null is returned.
@@ -184,6 +209,9 @@ public function getSMTPConnection(string $name);
* The indices of the array should act as the names of the accounts,
* and the value of the index should be an object of type SMTPAccount.
*
+ * Note: SMTP account properties that use the 'env:' prefix in configuration
+ * will be automatically resolved to their environment variable values.
+ *
* @return array An associative array that contains all added SMTP accounts.
*
*/
diff --git a/WebFiori/Framework/Config/Controller.php b/WebFiori/Framework/Config/Controller.php
index fc04303d6..9ba8fbb62 100644
--- a/WebFiori/Framework/Config/Controller.php
+++ b/WebFiori/Framework/Config/Controller.php
@@ -112,14 +112,38 @@ public static function setDriver(ConfigurationDriver $driver) {
/**
* Resolves environment variable references in configuration values.
*
- * If value starts with 'env:', attempts to read from environment.
- * Falls back to original value if env var doesn't exist.
+ * This method enables the use of environment variables in configuration files
+ * by using the 'env:' prefix. When a configuration value starts with 'env:',
+ * the method attempts to read the corresponding environment variable.
+ *
+ * Example usage in JSON configuration:
+ *
+ * {
+ * "database-connections": {
+ * "production": {
+ * "host": "env:DB_HOST",
+ * "password": "env:DB_PASS"
+ * }
+ * }
+ * }
+ *
+ *
+ * The method will:
+ * - Check if the value starts with 'env:'
+ * - Extract the environment variable name (e.g., 'DB_HOST' from 'env:DB_HOST')
+ * - Attempt to read from getenv() first, then $_ENV
+ * - Fall back to the original value if the environment variable doesn't exist
*
- * @param mixed $value The value to resolve
+ * @param mixed $value The value to resolve. Can be any type, but only strings
+ * starting with 'env:' will be processed.
*
- * @return mixed The resolved value
+ * @return mixed The resolved value. Returns the environment variable value if found,
+ * otherwise returns the original value unchanged.
*/
- public static function resolveEnvValue(string $value) {
+ public static function resolveEnvValue($value) {
+ if (!is_string($value)) {
+ return $value;
+ }
if (str_starts_with($value, 'env:')) {
$envVar = substr($value, 4);
diff --git a/WebFiori/Framework/Config/JsonDriver.php b/WebFiori/Framework/Config/JsonDriver.php
index 440c907fb..272b46d91 100644
--- a/WebFiori/Framework/Config/JsonDriver.php
+++ b/WebFiori/Framework/Config/JsonDriver.php
@@ -81,6 +81,9 @@ public function __construct() {
* a named constant at run time using the function 'define'. This means
* the constant will be accessable anywhere within the application's environment.
*
+ * Note: The value parameter supports the 'env:' prefix for referencing
+ * system environment variables (e.g., "env:MY_VAR").
+ *
* @param string $name The name of the named constant such as 'MY_CONSTANT'.
*
* @param mixed $value The value of the constant.
@@ -95,6 +98,15 @@ public function addEnvVar(string $name, mixed $value = null, ?string $descriptio
], 'none', 'same'));
$this->writeJson();
}
+ /**
+ * Adds new database connections information or update existing connection.
+ *
+ * Note: When using this driver, connection properties support environment variable
+ * substitution using the 'env:' prefix. For example, you can set host to "env:DB_HOST"
+ * in the JSON configuration file, and it will be resolved at runtime.
+ *
+ * @param ConnectionInfo $dbConnectionsInfo An object which holds connection information.
+ */
public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo) {
$connectionJAsJson = new Json([
'type' => $dbConnectionsInfo->getDatabaseType(),
@@ -108,7 +120,15 @@ public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo) {
$this->json->get('database-connections')->add($dbConnectionsInfo->getName(), $connectionJAsJson);
$this->writeJson();
}
-
+ /**
+ * Adds new SMTP account or Updates an existing one.
+ *
+ * Note: When using this driver, SMTP account properties support environment variable
+ * substitution using the 'env:' prefix. For example, you can set password to "env:SMTP_PASS"
+ * in the JSON configuration file, and it will be resolved at runtime.
+ *
+ * @param SMTPAccount $emailAccount An instance of 'SMTPAccount'.
+ */
public function addOrUpdateSMTPAccount(SMTPAccount $emailAccount) {
$connectionAsJson = new Json([
'host' => $emailAccount->getServerAddress(),
@@ -190,6 +210,16 @@ public static function getConfigFileName() : string {
return self::$configFileName;
}
+ /**
+ * Returns database connection information given connection name.
+ *
+ * Note: Connection properties that use the 'env:' prefix in configuration
+ * will be automatically resolved to their environment variable values.
+ *
+ * @param string $conName The name of the connection.
+ *
+ * @return ConnectionInfo|null Returns connection info if found, null otherwise.
+ */
public function getDBConnection(string $conName) {
$jsonObj = $this->json->get('database-connections')->get($conName);
@@ -218,6 +248,9 @@ public function getDBConnection(string $conName) {
/**
* Returns an associative array that contain the information of database connections.
*
+ * Note: Connection properties that use the 'env:' prefix in configuration
+ * will be automatically resolved to their environment variable values.
+ *
* @return array An associative array. The indices are connections names and
* values are objects of type 'ConnectionInfo'.
*/
@@ -281,6 +314,9 @@ public function getDescriptions(): array {
* Returns an array that holds the information of defined application environment
* variables.
*
+ * Note: Environment variable values that use the 'env:' prefix in configuration
+ * will be automatically resolved to their system environment variable values.
+ *
* @return array The returned array will be associative. The key will represent
* the name of the variable and its value is a sub-associative array with
* two indices, 'description' and 'value'. The description index is a text that describes
@@ -338,6 +374,9 @@ public function getPrimaryLanguage(): string {
* return the hashed value. If no password is set, this method will return the
* string 'NO_PASSWORD'.
*
+ * Note: The scheduler password value supports environment variable substitution
+ * using the 'env:' prefix (e.g., "env:SCHEDULER_PASS" in configuration).
+ *
* @return string Password hash or the string 'NO_PASSWORD' if there is no
* password.
*/
@@ -353,10 +392,12 @@ public function getSchedulerPassword(): string {
/**
* Returns SMTP connection given its name.
*
- * The method will search
- * for an account with the given name in the set
+ * The method will search for an account with the given name in the set
* of added accounts. If no account was found, null is returned.
*
+ * Note: SMTP account properties that use the 'env:' prefix in configuration
+ * will be automatically resolved to their environment variable values.
+ *
* @param string $name The name of the account.
*
* @return SMTPAccount|null If the account is found, The method
@@ -383,6 +424,9 @@ public function getSMTPConnection(string $name) {
/**
* Returns an array that contains all added SMTP accounts.
*
+ * Note: SMTP account properties that use the 'env:' prefix in configuration
+ * will be automatically resolved to their environment variable values.
+ *
* @return array An array that contains all added SMTP accounts.
*/
public function getSMTPConnections(): array {
From 89cd39d0f48c558a471a6351218539a8c72f30b2 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 18 Feb 2026 01:07:38 +0300
Subject: [PATCH 86/88] feat: Add `env:` to Class Driver
---
WebFiori/Framework/Config/ClassDriver.php | 112 ++++++++++++++-
.../Tests/Config/ClassDriverEnvTest.php | 128 ++++++++++++++++++
2 files changed, 234 insertions(+), 6 deletions(-)
create mode 100644 tests/WebFiori/Framework/Tests/Config/ClassDriverEnvTest.php
diff --git a/WebFiori/Framework/Config/ClassDriver.php b/WebFiori/Framework/Config/ClassDriver.php
index 3c4647836..437d0c793 100644
--- a/WebFiori/Framework/Config/ClassDriver.php
+++ b/WebFiori/Framework/Config/ClassDriver.php
@@ -15,6 +15,18 @@
* create a class called 'AppConfig' on the directory APP_DIR/Config and
* use it to read and write configurations.
*
+ * ## Environment Variable Support
+ *
+ * This driver supports environment variable substitution using the 'env:' prefix.
+ * The following configuration sections support this feature:
+ * - Database connections (all properties)
+ * - SMTP connections (all properties)
+ * - Environment variables (value field)
+ * - Scheduler password
+ *
+ * Values stored in the generated PHP class can use the 'env:' prefix, and they
+ * will be resolved to their environment variable values when read.
+ *
* @author Ibrahim
*/
class ClassDriver implements ConfigurationDriver {
@@ -60,6 +72,9 @@ public static function a($file, $str, $tabSize = 0) {
* a named constant at run time using the function 'define'. This means
* the constant will be accesaable anywhere within the appllication's environment.
*
+ * Note: The value parameter supports the 'env:' prefix for referencing
+ * system environment variables (e.g., "env:MY_VAR").
+ *
* @param string $name The name of the named constant such as 'MY_CONSTANT'.
*
* @param mixed $value The value of the constant.
@@ -77,6 +92,10 @@ public function addEnvVar(string $name, mixed $value = null, ?string $descriptio
/**
* Adds new database connections information or update existing connection.
*
+ * Note: When using this driver, connection properties support environment variable
+ * substitution using the 'env:' prefix. Values will be stored in the generated PHP class
+ * and resolved when read.
+ *
* @param ConnectionInfo $dbConnectionsInfo An object which holds connection information.
*/
public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo) {
@@ -86,6 +105,10 @@ public function addOrUpdateDBConnection(ConnectionInfo $dbConnectionsInfo) {
/**
* Adds new SMTP account or Updates an existing one.
*
+ * Note: When using this driver, SMTP account properties support environment variable
+ * substitution using the 'env:' prefix. Values will be stored in the generated PHP class
+ * and resolved when read.
+ *
* @param SMTPAccount $emailAccount An instance of 'SMTPAccount'.
*/
public function addOrUpdateSMTPAccount(SMTPAccount $emailAccount) {
@@ -157,6 +180,9 @@ public function getBaseURL(): string {
/**
* Returns database connection information given connection name.
*
+ * Note: Connection properties that use the 'env:' prefix will be
+ * automatically resolved to their environment variable values.
+ *
* @param string $conName The name of the connection.
*
* @return ConnectionInfo|null The method will return an object of type
@@ -174,10 +200,30 @@ public function getDBConnection(string $conName) {
/**
* Returns an associative array that contain the information of database connections.
*
+ * Note: Connection properties that use the 'env:' prefix will be
+ * automatically resolved to their environment variable values.
+ *
* @return array An associative array of objects of type ConnectionInfo.
*/
public function getDBConnections(): array {
- return $this->configVars['database-connections'];
+ $connections = $this->configVars['database-connections'];
+
+ foreach ($connections as $name => $connObj) {
+ if ($connObj instanceof ConnectionInfo) {
+ $connObj->setHost(Controller::resolveEnvValue($connObj->getHost()));
+ $connObj->setUsername(Controller::resolveEnvValue($connObj->getUsername()));
+ $connObj->setPassword(Controller::resolveEnvValue($connObj->getPassword()));
+ $connObj->setDBName(Controller::resolveEnvValue($connObj->getDBName()));
+
+ $extras = $connObj->getExtars();
+ foreach ($extras as $key => $value) {
+ $extras[$key] = Controller::resolveEnvValue($value);
+ }
+ $connObj->setExtras($extras);
+ }
+ }
+
+ return $connections;
}
public function getDescription(string $langCode) {
@@ -198,12 +244,25 @@ public function getDescriptions(): array {
/**
* Returns an associative array of application constants.
*
+ * Note: Environment variable values that use the 'env:' prefix will be
+ * automatically resolved to their system environment variable values.
+ *
* @return array The indices of the array are names of the constants and
* values are sub-associative arrays. Each sub-array will have two indices,
* 'value' and 'description'.
*/
public function getEnvVars(): array {
- return $this->configVars['env-vars'];
+ $vars = $this->configVars['env-vars'];
+
+ foreach ($vars as $name => $varData) {
+ if (is_array($varData) && isset($varData['value'])) {
+ $vars[$name]['value'] = Controller::resolveEnvValue($varData['value']);
+ } else {
+ $vars[$name] = Controller::resolveEnvValue($varData);
+ }
+ }
+
+ return $vars;
}
/**
* Returns a string that represents the URL of home page of the application.
@@ -221,9 +280,17 @@ public function getHomePage() : string {
public function getPrimaryLanguage() : string {
return $this->configVars['site']['primary-lang'];
}
-
+ /**
+ * Returns sha256 hash of the password which is used to prevent unauthorized
+ * access to run the tasks or access scheduler web interface.
+ *
+ * Note: The scheduler password value supports environment variable substitution
+ * using the 'env:' prefix (e.g., "env:SCHEDULER_PASS").
+ *
+ * @return string Password hash or the string 'NO_PASSWORD' if there is no password.
+ */
public function getSchedulerPassword(): string {
- return $this->configVars['scheduler-password'];
+ return Controller::resolveEnvValue($this->configVars['scheduler-password']);
}
public function getSMTPAccount(string $name) {
@@ -242,15 +309,48 @@ public function getSMTPAccount(string $name) {
* will return an object of type SMTPAccount. Else, the
* method will return null.
*
+ */ /**
+ * Returns SMTP connection given its name.
+ *
+ * Note: SMTP account properties that use the 'env:' prefix will be
+ * automatically resolved to their environment variable values.
+ *
+ * @param string $name The name of the account.
+ *
+ * @return SMTPAccount|null If the account is found, The method
+ * will return an object of type SMTPAccount. Else, the
+ * method will return null.
+ *
*/
public function getSMTPConnection(string $name) {
if (isset($this->getSMTPConnections()[$name])) {
return $this->getSMTPConnections()[$name];
}
}
-
+ /**
+ * Returns an array that contains all added SMTP accounts.
+ *
+ * Note: SMTP account properties that use the 'env:' prefix will be
+ * automatically resolved to their environment variable values.
+ *
+ * @return array An associative array of SMTPAccount objects.
+ */
public function getSMTPConnections(): array {
- return $this->configVars['smtp-connections'];
+ $connections = $this->configVars['smtp-connections'];
+
+ foreach ($connections as $name => $smtpObj) {
+ if ($smtpObj instanceof SMTPAccount) {
+ $smtpObj->setServerAddress(Controller::resolveEnvValue($smtpObj->getServerAddress()));
+ $smtpObj->setPort(Controller::resolveEnvValue($smtpObj->getPort()));
+ $smtpObj->setUsername(Controller::resolveEnvValue($smtpObj->getUsername()));
+ $smtpObj->setPassword(Controller::resolveEnvValue($smtpObj->getPassword()));
+ $smtpObj->setAddress(Controller::resolveEnvValue($smtpObj->getAddress()));
+ $smtpObj->setSenderName(Controller::resolveEnvValue($smtpObj->getSenderName()));
+ $smtpObj->setAccessToken(Controller::resolveEnvValue($smtpObj->getAccessToken()));
+ }
+ }
+
+ return $connections;
}
public function getTheme(): string {
diff --git a/tests/WebFiori/Framework/Tests/Config/ClassDriverEnvTest.php b/tests/WebFiori/Framework/Tests/Config/ClassDriverEnvTest.php
new file mode 100644
index 000000000..51e579948
--- /dev/null
+++ b/tests/WebFiori/Framework/Tests/Config/ClassDriverEnvTest.php
@@ -0,0 +1,128 @@
+initialize(true);
+
+ // Add connection with env: values
+ $conn = new ConnectionInfo('mysql', 'env:TEST_DB_USER', 'env:TEST_DB_PASS', 'env:TEST_DB_NAME');
+ $conn->setHost('env:TEST_DB_HOST');
+ $conn->setName('test_conn');
+ $driver->addOrUpdateDBConnection($conn);
+
+ // Retrieve and verify resolution
+ $retrieved = $driver->getDBConnection('test_conn');
+ $this->assertEquals('192.168.1.100', $retrieved->getHost());
+ $this->assertEquals('testuser', $retrieved->getUsername());
+ $this->assertEquals('testpass123', $retrieved->getPassword());
+ $this->assertEquals('testdb', $retrieved->getDBName());
+
+ $driver->remove();
+ }
+
+ /**
+ * @test
+ */
+ public function testEnvVarsInSMTPConnection() {
+ putenv('TEST_SMTP_HOST=smtp.test.com');
+ putenv('TEST_SMTP_USER=test@example.com');
+ putenv('TEST_SMTP_PASS=smtppass');
+ putenv('TEST_SMTP_FROM=noreply@test.com');
+
+ $driver = new ClassDriver();
+ $driver->initialize(true);
+
+ // Add SMTP with env: values
+ $smtp = new SMTPAccount();
+ $smtp->setAccountName('test_smtp');
+ $smtp->setServerAddress('env:TEST_SMTP_HOST');
+ $smtp->setUsername('env:TEST_SMTP_USER');
+ $smtp->setPassword('env:TEST_SMTP_PASS');
+ $smtp->setAddress('env:TEST_SMTP_FROM');
+ $smtp->setSenderName('Test Sender');
+ $smtp->setPort(587);
+ $driver->addOrUpdateSMTPAccount($smtp);
+
+ // Retrieve and verify resolution
+ $retrieved = $driver->getSMTPConnection('test_smtp');
+ $this->assertEquals('smtp.test.com', $retrieved->getServerAddress());
+ $this->assertEquals('test@example.com', $retrieved->getUsername());
+ $this->assertEquals('smtppass', $retrieved->getPassword());
+ $this->assertEquals('noreply@test.com', $retrieved->getAddress());
+
+ $driver->remove();
+ }
+
+ /**
+ * @test
+ */
+ public function testEnvVarsInEnvVars() {
+ putenv('TEST_API_KEY=secret_key_123');
+
+ $driver = new ClassDriver();
+ $driver->initialize(true);
+
+ $driver->addEnvVar('MY_API_KEY', 'env:TEST_API_KEY', 'API Key from environment');
+
+ $vars = $driver->getEnvVars();
+ $this->assertEquals('secret_key_123', $vars['MY_API_KEY']['value']);
+
+ $driver->remove();
+ }
+
+ /**
+ * @test
+ */
+ public function testEnvVarsInSchedulerPassword() {
+ putenv('TEST_SCHEDULER_PASS=hashed_password_123');
+
+ $driver = new ClassDriver();
+ $driver->initialize(true);
+
+ $driver->setSchedulerPassword('env:TEST_SCHEDULER_PASS');
+
+ $this->assertEquals('hashed_password_123', $driver->getSchedulerPassword());
+
+ $driver->remove();
+ }
+
+ /**
+ * @test
+ */
+ public function testFallbackWhenEnvNotSet() {
+ $driver = new ClassDriver();
+ $driver->initialize(true);
+
+ // Add connection with env: that doesn't exist
+ $conn = new ConnectionInfo('mysql', 'env:NONEXISTENT_USER', 'pass', 'db');
+ $conn->setHost('env:NONEXISTENT_HOST');
+ $conn->setName('fallback_test');
+ $driver->addOrUpdateDBConnection($conn);
+
+ // Should fallback to original value
+ $retrieved = $driver->getDBConnection('fallback_test');
+ $this->assertEquals('env:NONEXISTENT_HOST', $retrieved->getHost());
+ $this->assertEquals('env:NONEXISTENT_USER', $retrieved->getUsername());
+
+ $driver->remove();
+ }
+}
From e7429c16cbea0999f2c2f07a333ec1f54e0bd971 Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 18 Feb 2026 01:57:01 +0300
Subject: [PATCH 87/88] Update JsonDriverTest.php
---
tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
index 4919d2ba6..e8ce8ebf6 100644
--- a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
+++ b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php
@@ -664,5 +664,6 @@ public function testEnvVars00() {
putenv('SCHEDULER_PASS=my_secure_hash');
$this->assertEquals('my_secure_hash', $driver->getSchedulerPassword());
+ $driver->setConfigFileName('app-config.json');
}
}
From a8ee6672e2758e919dc7b2bfcc6e8d3718cab82f Mon Sep 17 00:00:00 2001
From: Ibrahim BinAlshikh
Date: Wed, 18 Feb 2026 02:24:36 +0300
Subject: [PATCH 88/88] fix: App Directories Creation
---
WebFiori/Framework/App.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php
index 0962a778b..b3f62091b 100644
--- a/WebFiori/Framework/App.php
+++ b/WebFiori/Framework/App.php
@@ -726,6 +726,7 @@ private static function getRoot() {
* @throws Exception
*/
private static function initAutoLoader() {
+ Ini::createAppDirs();
/**
* Initialize autoloader.
*/
@@ -744,7 +745,6 @@ private static function initAutoLoader() {
}
if (!class_exists(APP_DIR.'\\Ini\\AutoLoad')) {
- Ini::createAppDirs();
Ini::get()->createIniClass('AutoLoad', 'Add user-defined directories to the set of directories at which the framework will search for classes.');
}
self::call(APP_DIR.'\\Ini\\AutoLoad::initialize');