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 diff --git a/.github/workflows/php81.yaml b/.github/workflows/php81.yaml index 0f9bfadd2..83660001d 100644 --- a/.github/workflows/php81.yaml +++ b/.github/workflows/php81.yaml @@ -1,10 +1,15 @@ -name: Build PHP 8.1 +name: PHP 8.1 on: push: branches: [ main ] pull_request: branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: test: runs-on: ubuntu-latest @@ -104,7 +109,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..df1fd71e0 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: @@ -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 @@ -105,7 +109,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..594ca1f56 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: @@ -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 @@ -105,7 +109,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..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 @@ -106,23 +110,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..5f2ba4d24 --- /dev/null +++ b/.github/workflows/php85.yml @@ -0,0 +1,132 @@ +name: PHP 8.5 + +on: + push: + branches: [ main, dev ] + pull_request: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +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:12.5.4, 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 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 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 diff --git a/App/Database/empl/temp b/App/Database/Empl/temp similarity index 100% rename from App/Database/empl/temp rename to App/Database/Empl/temp diff --git a/App/Database/migrations/.gitkeep b/App/Database/Migrations/.gitkeep similarity index 100% rename from App/Database/migrations/.gitkeep rename to App/Database/Migrations/.gitkeep diff --git a/App/Database/migrations/commands/.gitkeep b/App/Database/Migrations/Commands/.gitkeep similarity index 100% rename from App/Database/migrations/commands/.gitkeep rename to App/Database/Migrations/Commands/.gitkeep diff --git a/App/Database/migrations/emptyRunner/XRunner.php b/App/Database/Migrations/EmptyRunner/XRunner.php similarity index 100% rename from App/Database/migrations/emptyRunner/XRunner.php rename to App/Database/Migrations/EmptyRunner/XRunner.php diff --git a/App/Database/migrations/multi/Migration000.php b/App/Database/Migrations/Multi/Migration000.php similarity index 98% rename from App/Database/migrations/multi/Migration000.php rename to 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 similarity index 93% rename from App/Database/migrations/multi/Migration001.php rename to 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 similarity index 97% rename from App/Database/migrations/multi/Migration002.php rename to 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 similarity index 86% rename from App/Database/migrations/multi/MultiRunner.php rename to 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 similarity index 95% rename from App/Database/migrations/multiDownErr/Migration000.php rename to 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 similarity index 98% rename from App/Database/migrations/multiDownErr/Migration001.php rename to 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 similarity index 98% rename from App/Database/migrations/multiDownErr/Migration002.php rename to 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 similarity index 91% rename from App/Database/migrations/multiDownErr/MultiErrRunner.php rename to 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 similarity index 98% rename from App/Database/migrations/multiErr/Migration000.php rename to 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 similarity index 98% rename from App/Database/migrations/multiErr/Migration001.php rename to 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 similarity index 98% rename from App/Database/migrations/multiErr/Migration002.php rename to 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 similarity index 98% rename from App/Database/migrations/multiErr/MultiErrRunner.php rename to 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 similarity index 98% rename from App/Database/migrations/noConn/Migration000.php rename to 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 similarity index 97% rename from App/Database/migrations/noConn/XRunner.php rename to 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/App/Database/super/temp b/App/Database/Super/temp similarity index 100% rename from App/Database/super/temp rename to App/Database/Super/temp 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 @@ -

- + @@ -34,7 +34,7 @@ WebFiori Framework is a mini web development framework which is built using PHP || || |
| - +|
| ## Key Features diff --git a/WebFiori/Framework/App.php b/WebFiori/Framework/App.php index df2feecc0..b3f62091b 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,174 @@ 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\Commands')) { + Ini::get()->createIniClass('Commands', 'A method that can be used to register custom 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\\CreateMigrationCommand', + '\\WebFiori\\Framework\\Cli\\Commands\\CreateSeederCommand', + + + + + + '\\WebFiori\\Framework\\Cli\\Commands\\RunMigrationsCommandNew', + '\\WebFiori\\Framework\\Cli\\Commands\\RollbackMigrationsCommand', + '\\WebFiori\\Framework\\Cli\\Commands\\InitMigrationsCommand', + '\\WebFiori\\Framework\\Cli\\Commands\\DryRunMigrationsCommand', + '\\WebFiori\\Framework\\Cli\\Commands\\MigrationsStatusCommand', + '\\WebFiori\\Framework\\Cli\\Commands\\FreshMigrationsCommand', + ]; + + foreach ($commands as $c) { + $r->register(new $c()); + } + $r->setDefaultCommand('help'); + self::call(APP_DIR.'\Ini\Commands::initialize'); + }); + } + + 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: + *

+ */ + 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 +472,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,29 +489,36 @@ 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_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(); @@ -396,71 +526,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\\CreateCommand', - '\\WebFiori\\Framework\\Cli\\Commands\\AddCommand', - - - - - - '\\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. * @@ -495,7 +560,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) { @@ -526,12 +591,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"); @@ -544,7 +611,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 @@ -639,64 +705,54 @@ 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 * @throws Exception */ private static function initAutoLoader() { + Ini::createAppDirs(); /** * 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.'); + if (class_exists('WebFiori\Framework\Autoload\ClassLoader', false)) { + return; + } + $isLoaded = false; + + 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; } - require_once $autoloader; + + if (!class_exists(APP_DIR.'\\Ini\\AutoLoad')) { + 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'); } - 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.'); + if (!$isLoaded) { + throw new Exception('Unable to locate the autoloader class.'); } - self::call(APP_DIR.'\\Init\\InitAutoLoad::init'); - } - /** - * Initialize global constants which has information about framework version. - * - * The constants which are defined by this method include the following: - * - */ - 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'); } /** @@ -708,11 +764,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 @@ -721,10 +777,16 @@ 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) { @@ -743,8 +805,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))) { @@ -753,7 +815,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(); } } @@ -779,21 +841,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()); } } 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..2f4884725 --- /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->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...'); + + $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..c5d88aac6 --- /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->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()))); + $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/WebFiori/Framework/Cli/Commands/CreateCommand.php b/WebFiori/Framework/Cli/Commands/CreateCommand.php deleted file mode 100644 index bc5c2d3d6..000000000 --- a/WebFiori/Framework/Cli/Commands/CreateCommand.php +++ /dev/null @@ -1,158 +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 == 'Entity class from table.') { - $this->createEntityFromQuery(); - } else if ($answer == 'Web service.') { - $create = new CreateWebService($this); - $create->readClassInfo(); - } 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 == '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['entity'] = 'Entity class from table.'; - $options['web-service'] = 'Web service.'; - $options['task'] = 'Background Task.'; - $options['middleware'] = 'Middleware.'; - $options['command'] = 'CLI Command.'; - $options['theme'] = 'Theme.'; - $options['db'] = 'Database access class based on table.'; - $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/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/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/Cli/Commands/CreateMiddlewareCommand.php b/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php new file mode 100644 index 000000000..1971740e0 --- /dev/null +++ b/WebFiori/Framework/Cli/Commands/CreateMiddlewareCommand.php @@ -0,0 +1,124 @@ +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 && 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)); + }, '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/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php b/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php new file mode 100644 index 000000000..789e65058 --- /dev/null +++ b/WebFiori/Framework/Cli/Commands/CreateMigrationCommand.php @@ -0,0 +1,194 @@ +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) { + $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); + } + + return trim($className); + } + + private function getMigrationDescription(): string { + $description = $this->getArgValue('--description'); + + if ($description === null && $this->getArgValue('--class-name') === null) { + $description = $this->getInput('Enter migration description:', 'No description'); + } else if ($description === null) { + $description = 'No description'; + } + + return trim($description); + } + + private function getEnvironments(): array { + $environments = $this->getArgValue('--environments'); + + if ($environments !== null) { + return array_map('trim', explode(',', $environments)); + } + + 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/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/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/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php b/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php new file mode 100644 index 000000000..9eef592db --- /dev/null +++ b/WebFiori/Framework/Cli/Commands/CreateSeederCommand.php @@ -0,0 +1,194 @@ +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) { + $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); + } + + return trim($className); + } + + private function getSeederDescription(): string { + $description = $this->getArgValue('--description'); + + if ($description === null && $this->getArgValue('--class-name') === null) { + $description = $this->getInput('Enter seeder description:', 'No description'); + } else if ($description === null) { + $description = 'No description'; + } + + return trim($description); + } + + private function getEnvironments(): array { + $environments = $this->getArgValue('--environments'); + + if ($environments !== null) { + return array_map('trim', explode(',', $environments)); + } + + 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/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/WebFiori/Framework/Cli/Commands/CreateTableCommand.php b/WebFiori/Framework/Cli/Commands/CreateTableCommand.php new file mode 100644 index 000000000..6a7a593d5 --- /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::getSupportedDataTypes('mysql')); + + $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/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/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/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/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/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php b/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php new file mode 100644 index 000000000..49c8e7fb8 --- /dev/null +++ b/WebFiori/Framework/Cli/Commands/MigrationsStatusCommand.php @@ -0,0 +1,119 @@ +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 { + $allChanges = $this->runner->getChanges(); + $pending = $this->runner->getPendingChanges(false); + + // 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:'); + 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/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/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php b/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php deleted file mode 100644 index fb65f9de2..000000000 --- a/WebFiori/Framework/Cli/Commands/RunMigrationsCommand.php +++ /dev/null @@ -1,292 +0,0 @@ -initializeCommand()) { - return self::EXIT_ERROR; - } - - if ($this->isArgProvided('--init')) { - return $this->initializeMigrationsTable(); - } - - if ($this->isArgProvided('--rollback')) { - return $this->executeRollback(); - } - - return $this->executeMigrations(); - - } 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; - } - - 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(); - } - - $connections = App::getConfig()->getDBConnections(); - if (empty($connections)) { - $this->info('No connections were found in application configuration.'); - return null; - } - - $connectionName = $this->getArgValue('--connection'); - - if ($connectionName !== null) { - $connection = App::getConfig()->getDBConnection($connectionName); - if ($connection === null) { - $this->error("No connection was found which has the name '$connectionName'."); - return null; - } - return $connection; - } - - return CLIUtils::getConnectionName($this); - } - - /** - * Initialize migrations table. - */ - private function initializeMigrationsTable(): int { - try { - $this->println("Initializing migrations table..."); - $this->runner->createSchemaTable(); - $this->success("Migrations table successfully created."); - return self::EXIT_SUCCESS; - } catch (Throwable $e) { - $this->error('Unable to create migrations table due to following:'); - $this->println($e->getMessage()); - return self::EXIT_ERROR; - } - } - - /** - * 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..."); - - try { - if ($this->isArgProvided('--all')) { - $rolledBack = $this->runner->rollbackUpTo(null); - } else { - $rolledBack = $this->rollbackLast(); - } - - if (empty($rolledBack)) { - $this->info("No migrations were rolled back."); - } else { - foreach ($rolledBack as $migration) { - $this->success("Migration '{$migration->getName()}' was successfully rolled back."); - } - } - - 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; - } - } - - /** - * Rollback the last applied migration. - */ - private function rollbackLast(): array { - $changes = $this->runner->getChanges(); - $lastApplied = null; - - // Find the last applied migration - foreach ($changes as $change) { - if ($this->runner->isApplied($change->getName())) { - $lastApplied = $change; - } - } - - if ($lastApplied === null) { - return []; - } - - return $this->runner->rollbackUpTo($lastApplied->getName()); - } - - /** - * Execute migrations. - */ - private function executeMigrations(): int { - $migrations = $this->runner->getChanges(); - if (empty($migrations)) { - $this->info("No migrations found."); - return self::EXIT_SUCCESS; - } - - $this->println("Starting to execute migrations..."); - $appliedMigrations = []; - - 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 (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); - } - - 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; - } - } - return null; - } -} diff --git a/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php b/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php new file mode 100644 index 000000000..0dca325b5 --- /dev/null +++ b/WebFiori/Framework/Cli/Commands/RunMigrationsCommandNew.php @@ -0,0 +1,121 @@ +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(); + + $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/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/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/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 2dc0c16a6..000000000 --- a/WebFiori/Framework/Cli/Helpers/CreateMigration.php +++ /dev/null @@ -1,46 +0,0 @@ -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)); - } - public function isConfigured() : bool { - return $this->isConfigured; - } -} 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/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/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 367a44b5d..9ba8fbb62 100644 --- a/WebFiori/Framework/Config/Controller.php +++ b/WebFiori/Framework/Config/Controller.php @@ -109,6 +109,49 @@ public static function setDriver(ConfigurationDriver $driver) { self::get()->driver = $driver; self::init($driver); } + /** + * Resolves environment variable references in configuration values. + * + * 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. Can be any type, but only strings + * starting with 'env:' will be processed. + * + * @return mixed The resolved value. Returns the environment variable value if found, + * otherwise returns the original value unchanged. + */ + public static function resolveEnvValue($value) { + if (!is_string($value)) { + return $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 +160,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..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(), @@ -117,7 +137,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(); @@ -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); @@ -206,18 +236,21 @@ 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); } } /** * 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 @@ -291,9 +327,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 ]; } @@ -328,25 +374,30 @@ 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. */ 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. * - * 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 @@ -365,13 +416,17 @@ 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 + 'account-name' => $name, + 'access-token' => $this->getProp($jsonObj, 'access-token', $name, false) ]); } } /** * 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 { @@ -390,6 +445,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; } @@ -728,14 +784,14 @@ 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.'); } - return $val; + return Controller::resolveEnvValue($val); } private function isValidLangCode($langCode) { $code = strtoupper(trim($langCode)); 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, "isRequestMethodAllowed()) { + + if ($route->isRequestMethodAllowed((App::getRequest()->getMethod()))) { $this->uriObj = $route; foreach ($route->getMiddleware() as $mw) { 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'); } } 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/AttributeTableWriter.php b/WebFiori/Framework/Writers/AttributeTableWriter.php new file mode 100644 index 000000000..e14ed4b7c --- /dev/null +++ b/WebFiori/Framework/Writers/AttributeTableWriter.php @@ -0,0 +1,93 @@ +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) { + $type = strtoupper($col['type']); + $attr = "#[Column(name: '{$col['name']}', type: DataType::{$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"; + } + 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); + } + } + + public function writeClassDeclaration() { + $this->append('class '.$this->getName().' {'); + } +} diff --git a/WebFiori/Framework/Writers/ClassWriter.php b/WebFiori/Framework/Writers/ClassWriter.php index ed7675442..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,15 +24,13 @@ abstract class ClassWriter { * * @var string * - * @since 1.0 */ - private $classAsStr; + private $classLines; /** * The name of the class that will be created. * * @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. * @@ -110,18 +109,18 @@ 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') { $this->a($strOrArr, $tabsCount); - return; + return $this; } foreach ($strOrArr as $str) { $this->a($str, $tabsCount); } + return $this; } /** * Adds method definition to the class. @@ -139,29 +138,347 @@ 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 ClassWriter For chaining + */ + public function method( + string $funcName, + array $argsArr = [], + ?string $returns = null, + string $visibility = 'public', + bool $isStatic = false, + bool $isAbstract = false, + bool $isFinal = false + ) { + $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.' {'; + + $this->append($signature . $argsPart . ($isAbstract ? ';' : ' {'), 1); + return $this; + } + /** + * 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+) + * + * @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, + string $visibility = 'private', + ?string $type = null, + ?string $defaultValue = null, + bool $isStatic = false, + bool $isReadonly = false + ) { + $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; + } + + $this->append($declaration . ';', 1); + return $this; + } + /** + * Generate a constant declaration. + * + * @param string $name Constant name + * @param string $value Constant value as string + * @param string $visibility Visibility: 'public', 'protected', 'private' + * + * @return ClassWriter For chaining + */ + public function constant( + string $name, + string $value, + string $visibility = 'public' + ) { + $this->append($visibility . ' const ' . $name . ' = ' . $value . ';', 1); + return $this; + } + /** + * Add an empty line (fluent version). + * + * @return ClassWriter For chaining + */ + public function addEmptyLine() : ClassWriter { + $this->append(''); + return $this; + } + /** + * Start building a docblock. + * + * @param string $description Main description + * + * @return DocblockBuilder + */ + 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 ClassWriter For chaining + */ + public function classAttribute(string $name, array $params = [], int $indent = 0) : ClassWriter { + $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 ClassWriter For chaining + */ + public function propertyAttribute(string $name, array $params = [], int $indent = 1) : ClassWriter { + $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 ClassWriter For chaining + */ + public function methodAttribute(string $name, array $params = [], int $indent = 1) : ClassWriter { + $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; } + /** + * 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. * - * @since 1.0.1 */ public function getAbsolutePath() : string { return $this->getPath().DS.$this->getName().'.php'; @@ -178,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(); @@ -202,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; @@ -213,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; @@ -325,19 +639,20 @@ 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) : bool { + public function setClassName(string $name) : ClassWriter { $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; } /** @@ -345,36 +660,37 @@ public function setClassName(string $name) : bool { * * @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)) { - 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. * * @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) : bool { + public function setPath(string $path) : ClassWriter { $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. @@ -382,17 +698,51 @@ public function setPath(string $path) : bool { * @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) : bool { - if (self::isValidClassName($classNameSuffix)) { - $this->suffix = $classNameSuffix; - $this->className = $this->fixClassName($this->className); - - return true; + public function setSuffix(string $classNameSuffix) : ClassWriter { + 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; + } + /** + * 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. @@ -400,20 +750,27 @@ public function setSuffix(string $classNameSuffix) : bool { * 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()); $classFile->remove(); - $this->classAsStr = ''; + $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($this->classAsStr); - $classFile->write(false, true); - } + return implode("\n", $this->normalizeCode($this->classLines)); + } public abstract function writeClassBody(); /** * Writes the top section of the class that contains class comment. @@ -446,9 +803,25 @@ 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) { + 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 == '') { diff --git a/WebFiori/Framework/Writers/CommandClassWriter.php b/WebFiori/Framework/Writers/CommandClassWriter.php index cbbb39649..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;', @@ -169,13 +169,16 @@ 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.', - ' */', - $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/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/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/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; + } +} diff --git a/WebFiori/Framework/Writers/DomainEntityWriter.php b/WebFiori/Framework/Writers/DomainEntityWriter.php new file mode 100644 index 000000000..346b10d0a --- /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().' {'); + } + + protected function writeConstructor(array $params = [], $body = '', string $description = 'Creates new instance of the class.', int $indent = 1) { + $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); + } +} 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/WebFiori/Framework/Writers/MiddlewareClassWriter.php b/WebFiori/Framework/Writers/MiddlewareClassWriter.php index 3eb89a583..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); @@ -184,14 +184,17 @@ 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.', - ' */', - $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/MigrationClassWriter.php b/WebFiori/Framework/Writers/MigrationClassWriter.php new file mode 100644 index 000000000..1376bc930 --- /dev/null +++ b/WebFiori/Framework/Writers/MigrationClassWriter.php @@ -0,0 +1,125 @@ +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('}'); + } + + public function writeClassComment() { + $this->append([ + '/**', + ' * '.$this->description, + ' *', + ' * @author Ibrahim', + ' */' + ]); + } + + 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); + $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/WebFiori/Framework/Writers/RepositoryWriter.php b/WebFiori/Framework/Writers/RepositoryWriter.php new file mode 100644 index 000000000..0056d48ed --- /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->f('getTableName', [], 'string'); + $this->append('return \''.$this->tableName.'\';', 2); + $this->append('}', 1); + $this->append('', 1); + } + + private function writeGetIdField() { + $this->f('getIdField', [], 'string'); + $this->append('return \''.$this->idField.'\';', 2); + $this->append('}', 1); + $this->append('', 1); + } + + private function writeToEntity() { + $entityShortName = basename(str_replace('\\', '/', $this->entityClass)); + $this->f('toEntity', ['row' => 'array'], 'object'); + $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->f('toArray', ['entity' => 'object'], 'array'); + $this->append('return [', 2); + + foreach ($this->properties as $prop) { + $this->append("'{$prop['name']}' => \$entity->{$prop['name']},", 3); + } + + $this->append('];', 2); + $this->append('}', 1); + } +} diff --git a/WebFiori/Framework/Writers/RestServiceWriter.php b/WebFiori/Framework/Writers/RestServiceWriter.php new file mode 100644 index 000000000..0f7edf50f --- /dev/null +++ b/WebFiori/Framework/Writers/RestServiceWriter.php @@ -0,0 +1,119 @@ +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\\RequestParam', + 'WebFiori\\Http\\Annotations\\ResponseBody', + 'WebFiori\\Http\\Annotations\\AllowAnonymous', + 'WebFiori\\Http\\ParamType' + ]); + } + + public function addMethod(string $httpMethod, string $methodName, array $params = [], string $returnType = 'array') { + $this->methods[] = [ + 'http' => $httpMethod, + 'name' => $methodName, + 'params' => $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('/**'); + $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 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 = "#[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); + } +} diff --git a/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php b/WebFiori/Framework/Writers/SchedulerTaskClassWriter.php index e4438c8b3..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); @@ -195,13 +195,16 @@ 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.', - ' */', - $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/SeederClassWriter.php b/WebFiori/Framework/Writers/SeederClassWriter.php new file mode 100644 index 000000000..dc226f3e0 --- /dev/null +++ b/WebFiori/Framework/Writers/SeederClassWriter.php @@ -0,0 +1,112 @@ +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('}'); + } + + public function writeClassComment() { + $this->append([ + '/**', + ' * '.$this->description, + ' *', + ' * @author Ibrahim', + ' */' + ]); + } + + 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); + $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/WebFiori/Framework/Writers/TableClassWriter.php b/WebFiori/Framework/Writers/TableClassWriter.php deleted file mode 100644 index 911fc32a0..000000000 --- a/WebFiori/Framework/Writers/TableClassWriter.php +++ /dev/null @@ -1,529 +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: - * - *
  • - * - * - * - * @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" - ." * \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'"; - } - } - } - private function writeConstructor() { - $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/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.'); diff --git a/WebFiori/Framework/Writers/WebServiceWriter.php b/WebFiori/Framework/Writers/WebServiceWriter.php deleted file mode 100644 index d659e812b..000000000 --- a/WebFiori/Framework/Writers/WebServiceWriter.php +++ /dev/null @@ -1,348 +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: - * - * - * @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); - } - private function writeConstructor() { - $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[] = ' * '; - $this->append($docArr); - } - } -} diff --git a/composer.json b/composer.json index 057e01a32..499287414 100644 --- a/composer.json +++ b/composer.json @@ -2,7 +2,7 @@ "name": "webfiori/framework", "description": "WebFiori framework. Made to make the web bloom.", "homepage": "https://webfiori.com", - "version": "3.0.0-Beta.27", + "version": "3.0.0-beta.31", "type": "library", "support": { "issues": "https://github.com/webfiori/framework/issues", @@ -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": { 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/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..d6e55a34c --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/AddDbConnectionCommandTest.php @@ -0,0 +1,127 @@ +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", + "******\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("***********\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 + */ + 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("***********\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/AddLangCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php new file mode 100644 index 000000000..b60c0b550 --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/AddLangCommandTest.php @@ -0,0 +1,81 @@ +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..175327718 --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/AddSmtpConnectionCommandTest.php @@ -0,0 +1,50 @@ +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", + "********\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/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/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/CreateCommandCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateCommandCommandTest.php new file mode 100644 index 000000000..6b51f926e --- /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/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/CreateEntityCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateEntityCommandTest.php new file mode 100644 index 000000000..ed9200b26 --- /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/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/CreateMiddlewareCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php new file mode 100644 index 000000000..0534ea7be --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/CreateMiddlewareCommandTest.php @@ -0,0 +1,229 @@ +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()); + $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'); + } + /** + * @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([ + "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 + */ + public function testCreateMiddlewareWithArgs01() { + $className = 'TestMd'.time(); + + $output = $this->executeMultiCommand([ + CreateMiddlewareCommand::class, + '--class-name' => $className, + '--name' => $className, + '--priority' => '0', + '--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'); + } + /** + * @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([ + "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 + */ + public function testCreateMiddlewareWithArgs03() { + $className = 'TestMd'.time(); + + $output = $this->executeMultiCommand([ + CreateMiddlewareCommand::class, + '--class-name' => $className, + '--name' => $className, + '--priority' => 'invalid', + '--groups' => '' + ]); + + $this->assertEquals([ + "Error: Priority must be a number.\n" + ], $output); + $this->assertEquals(-1, $this->getExitCode()); + } + /** + * @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([ + "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'); + } +} 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/CreateMigrationCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php new file mode 100644 index 000000000..70d9de0fb --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationCommandTest.php @@ -0,0 +1,100 @@ +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', + '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()); + $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/CreateMigrationTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php deleted file mode 100644 index 7dff77f2d..000000000 --- a/tests/WebFiori/Framework/Tests/Cli/CreateMigrationTest.php +++ /dev/null @@ -1,82 +0,0 @@ -assertEquals([ - "Migration namespace: Enter = 'App\Database\migrations'\n", - "Provide a name for the class that will have migration logic:\n", - 'Info: New class was created at "'. APP_PATH .'Database'.DS.'Migrations".'."\n", - ], $this->executeMultiCommand([ - CreateCommand::class, - '--c' => 'migration', - ], [ - "\n", - $name, - "Great One", - "11" - ])); - $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)); - $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; - } -} \ No newline at end of file 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/CreateRepositoryCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateRepositoryCommandTest.php new file mode 100644 index 000000000..8b0ddf450 --- /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/CreateSeederCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php new file mode 100644 index 000000000..a4dd92ce7 --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/CreateSeederCommandTest.php @@ -0,0 +1,100 @@ +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', + '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()); + $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/CreateServiceCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateServiceCommandTest.php new file mode 100644 index 000000000..1ea2f0108 --- /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/CreateTableCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php new file mode 100644 index 000000000..4075b30f8 --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/CreateTableCommandTest.php @@ -0,0 +1,232 @@ +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); + } + + /** + * @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/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/CreateTaskCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/CreateTaskCommandTest.php new file mode 100644 index 000000000..b3b8a27a8 --- /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/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()); - } -} 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/Cli/DryRunMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php new file mode 100644 index 000000000..0b1ab158c --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/DryRunMigrationsCommandTest.php @@ -0,0 +1,224 @@ +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()); + } + + /** + * @test + */ + public function testDryRunWithPendingMigration() { + $this->createTestMigration('TestMigration'); + $this->initMigrations(); + + $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()); + } + + /** + * @test + */ + public function testDryRunWithCustomEnv() { + $this->createTestMigration('EnvTestMigration'); + $this->initMigrations('staging'); + + $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'); + $this->initMigrations(); + + $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 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'; + + 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'; + + 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/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 2f3941bbe..e00b09b08 100644 --- a/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php +++ b/tests/WebFiori/Framework/Tests/Cli/HelpCommandTest.php @@ -11,24 +11,40 @@ 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", " --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", - " create: Creates a system entity (middleware, web service, background process ...).\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", + " 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", + " 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", + " create:migration: Create a new database migration class.\n", + " create:seeder: Create a new database seeder class.\n", - " 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", + " migrations:fresh: Rollback all migrations and run them fresh.\n", ], $this->executeMultiCommand([ 'help', ])); diff --git a/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php new file mode 100644 index 000000000..47218bdf1 --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/InitMigrationsCommandTest.php @@ -0,0 +1,119 @@ +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()); + } + + /** + * @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'); + App::getConfig()->addOrUpdateDBConnection($this->testConnection); + } + + protected function setUp(): void { + parent::setUp(); + $this->setupTestConnection(); + } + + protected function tearDown(): void { + App::getConfig()->removeAllDBConnections(); + parent::tearDown(); + } +} diff --git a/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php new file mode 100644 index 000000000..5e04b7e4c --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/MigrationsStatusCommandTest.php @@ -0,0 +1,211 @@ +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()); + } + + /** + * @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 = <<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/RollbackMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/RollbackMigrationsCommandTest.php new file mode 100644 index 000000000..01c56159b --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/RollbackMigrationsCommandTest.php @@ -0,0 +1,258 @@ +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()); + } + + /** + * @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 = <<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/RunMigrationsCommandNewTest.php b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandNewTest.php new file mode 100644 index 000000000..ca2043a07 --- /dev/null +++ b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandNewTest.php @@ -0,0 +1,212 @@ +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()); + } + + /** + * @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 = <<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(); + } + + 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 + } + } +} diff --git a/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php b/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php deleted file mode 100644 index ba8bd931d..000000000 --- a/tests/WebFiori/Framework/Tests/Cli/RunMigrationsCommandTest.php +++ /dev/null @@ -1,225 +0,0 @@ -setupTestConnection(); - } - - protected function tearDown(): void { - 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); - } - - /** - * @test - */ - public function testExecWithNoConnections(): void { - App::getConfig()->removeAllDBConnections(); - - $output = $this->executeMultiCommand([ - RunMigrationsCommand::class - ]); - - $this->assertContains("Info: No connections were found in application configuration.\n", $output); - $this->assertEquals(1, $this->getExitCode()); - } - - /** - * @test - */ - public function testExecWithInvalidConnection(): void { - $output = $this->executeMultiCommand([ - RunMigrationsCommand::class, - '--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->assertEquals(1, $this->getExitCode()); - } - - /** - * @test - */ - public function testInitializeMigrationsTable(): void { - $output = $this->executeMultiCommand([ - RunMigrationsCommand::class, - '--connection' => 'test-connection', - '--init' - ]); - - $this->assertContains("Initializing migrations table...\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 { - $output = $this->executeMultiCommand([ - RunMigrationsCommand::class, - '--connection' => 'test-connection' - ]); - - $this->assertContains("Info: No migrations found.\n", $output); - $this->assertEquals(0, $this->getExitCode()); - } - - /** - * @test - */ - public function testRollbackWithNoMigrations(): void { - $output = $this->executeMultiCommand([ - RunMigrationsCommand::class, - '--connection' => 'test-connection', - '--rollback' - ]); - - // Debug: Print actual output - $this->assertEquals(0, $this->getExitCode()); - } - - /** - * @test - */ - public function testRollbackAllWithNoMigrations(): void { - $output = $this->executeMultiCommand([ - RunMigrationsCommand::class, - '--connection' => 'test-connection', - '--rollback', - '--all' - ]); - - // 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); - $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 createFaultyMigrationRunner(): void { - $code = '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'; - } - - private function cleanupFaultyMigrationRunner(): void { - if (file_exists(APP_PATH . 'FaultyMigrationRunner.php')) { - unlink(APP_PATH . 'FaultyMigrationRunner.php'); - } - } -} 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/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(); + } +} diff --git a/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php b/tests/WebFiori/Framework/Tests/Config/JsonDriverTest.php index 732a41328..e8ce8ebf6 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 */ @@ -221,6 +143,288 @@ public function testAddEnvVar02() { ] ], $driver->getEnvVars()); } + /** + * @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 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 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 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 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 */ @@ -273,100 +477,50 @@ public function testSetTitle02() { /** * @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 - */ - public function testSetDescription00() { + public function testSetTitleSeparator00() { $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'); + $this->assertEquals('|', $driver->getTitleSeparator()); + $driver->setTitleSeparator('*'); } - /** * @test - * @depends testSetDescription00 + * @depends testSetTitleSeparator00 */ - public function testSetDescription01() { + public function testSetTitleSeparator01() { $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('*', $driver->getTitleSeparator()); + $driver->setTitleSeparator(''); } /** * @test - * @depends testSetDescription01 + * @depends testSetTitleSeparator01 */ - public function testSetDescription02() { + public function testSetTitleSeparator02() { $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 testSetTitleSeparator00() { - $driver = new JsonDriver(); - $this->assertEquals('|', $driver->getTitleSeparator()); - $driver->setTitleSeparator('*'); + $this->assertEquals('*', $driver->getTitleSeparator()); } /** * @test - * @depends testSetTitleSeparator00 */ - public function testSetTitleSeparator01() { + public function testSetVersion00() { $driver = new JsonDriver(); - $driver->initialize(); - $this->assertEquals('*', $driver->getTitleSeparator()); - $driver->setTitleSeparator(''); + $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 testSetTitleSeparator01 + * @depends testSetVersion00 */ - public function testSetTitleSeparator02() { + public function testSetVersion01() { $driver = new JsonDriver(); $driver->initialize(); - $this->assertEquals('*', $driver->getTitleSeparator()); + $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,42 @@ 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() { + 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(); - $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())); + $vars = $driver->getEnvVars(); - $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(); + $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()); + + putenv('SCHEDULER_PASS=my_secure_hash'); + $this->assertEquals('my_secure_hash', $driver->getSchedulerPassword()); $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()); } } 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()); 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(); - } -} 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()); - } -}