diff --git a/.github/workflows/code-analysis.yaml b/.github/workflows/code-analysis.yaml new file mode 100644 index 000000000..04c516d82 --- /dev/null +++ b/.github/workflows/code-analysis.yaml @@ -0,0 +1,63 @@ +name: Tests + +on: + push: + branches: + - "wip/1.2" + pull_request: + branches: + - "wip/1.2" + +jobs: + codeAnalysis: + runs-on: ubuntu-latest + name: Code Analysis + env: + extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip + key: winter-storm-cache-v1.2 + steps: + - name: Cancel previous incomplete runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} + + - name: Checkout changes + uses: actions/checkout@v2 + + - name: Setup extension cache + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: '8.0' + extensions: ${{ env.extensions }} + key: ${{ env.key }} + + - name: Cache extensions + uses: actions/cache@v2 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: Install PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + extensions: ${{ env.extensions }} + + - name: Setup dependency cache + id: composercache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composercache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install Composer dependencies + run: composer install --no-interaction --no-progress --no-scripts + + - name: Analyse code + run: ./vendor/bin/phpstan analyse diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 888641921..59d2e7f9d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,14 +14,19 @@ jobs: max-parallel: 6 matrix: operatingSystem: [ubuntu-latest, windows-latest] - phpVersion: ['7.2', '7.3', '7.4', '8.0'] + phpVersion: ['8.0', '8.1'] fail-fast: false runs-on: ${{ matrix.operatingSystem }} name: ${{ matrix.operatingSystem }} / PHP ${{ matrix.phpVersion }} env: extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip - key: winter-storm-cache-v1.1.2 + key: winter-storm-cache-v1.2 steps: + - name: Cancel previous incomplete runs + uses: styfle/cancel-workflow-action@0.8.0 + with: + access_token: ${{ github.token }} + - name: Checkout changes uses: actions/checkout@v2 @@ -44,7 +49,6 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.phpVersion }} - tools: composer:v2 extensions: ${{ env.extensions }} - name: Setup dependency cache @@ -62,7 +66,7 @@ jobs: run: composer install --no-interaction --no-progress --no-scripts - name: Setup problem matchers for PHPUnit - if: matrix.phpVersion == '7.4' + if: matrix.phpVersion == '8.1' run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" - name: Run tests diff --git a/.gitignore b/.gitignore index fb8ea7e51..50fa968a2 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,13 @@ composer.lock # Other files .DS_Store php_errors.log + +#eclipse +/.buildpath +/.project +/.settings/ + +#phpunit +tests/.phpunit.result.cache .phpunit.result.cache +tests/tmp diff --git a/composer.json b/composer.json index 9c81953a1..163a5a278 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php": ">=7.2.9", + "php": "^8.0.2", "ext-ctype": "*", "ext-curl": "*", "ext-dom": "*", @@ -32,27 +32,32 @@ "ext-libxml": "*", "ext-mbstring": "*", "ext-openssl": "*", - "ext-PDO": "*", + "ext-pdo": "*", "ext-zip": "*", + + "assetic/framework": "~3.0", "doctrine/dbal": "^2.6", "erusev/parsedown-extra": "~0.7", - "linkorb/jsmin-php": "~1.0", - "wikimedia/less.php": "~3.0", - "scssphp/scssphp": "~1.0", - "symfony/yaml": "^3.4", - "twig/twig": "~2.0", + "laravel/framework": "^9.1", + "laravel/tinker": "^2.7", "league/csv": "~9.1", "nesbot/carbon": "^2.0", - "laravel/framework": "~6.0", - "laravel/tinker": "~2.0" + "nikic/php-parser": "^4.10", + "scssphp/scssphp": "~1.0", + "symfony/yaml": "^6.0", + "twig/twig": "~3.0", + "wikimedia/less.php": "~3.0", + "wikimedia/minify": "~2.2" }, "require-dev": { - "phpunit/phpunit": "^8.5.12|^9.3.3", - "mockery/mockery": "~1.3.3|^1.4.2", - "squizlabs/php_codesniffer": "3.*", + "phpunit/phpunit": "^9.5.8", + "mockery/mockery": "^1.4.4", + "squizlabs/php_codesniffer": "^3.2", "php-parallel-lint/php-parallel-lint": "^1.0", "meyfa/phpunit-assert-gd": "^2.0.0|^3.0.0", - "dms/phpunit-arraysubset-asserts": "^0.1.0|^0.2.1" + "dms/phpunit-arraysubset-asserts": "^0.1.0|^0.2.1", + "nunomaduro/larastan": "^2.0.1", + "orchestra/testbench": "^7.1.0" }, "suggest": { "ext-pdo_dblib": "Required to use MS SQL Server databases", @@ -79,7 +84,10 @@ "classmap": [ "tests/TestCase.php", "tests/DbTestCase.php" - ] + ], + "psr-4": { + "Winter\\Storm\\Tests\\": "tests/" + } }, "scripts": { "test": [ diff --git a/phpcs.xml.dist b/phpcs.xml.dist index f7d16d946..48ed245f7 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -9,12 +9,13 @@ + + + - + */src/Auth/Migrations/*\.php */src/Database/Migrations/*\.php */tests/* @@ -28,6 +29,9 @@ */tests/* + + + src/ tests/ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 000000000..fd99a24a5 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,801 @@ +parameters: + ignoreErrors: + - + message: "#^Call to an undefined method \\$this\\(Winter\\\\Storm\\\\Auth\\\\Models\\\\Group\\)\\:\\:getOriginalEncryptableValues\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/Group.php + + - + message: "#^Call to an undefined method \\$this\\(Winter\\\\Storm\\\\Auth\\\\Models\\\\Group\\)\\:\\:getOriginalHashValues\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/Group.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Auth\\\\Models\\\\Group\\:\\:afterValidate\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/Group.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Auth\\\\Models\\\\Group\\:\\:beforeValidate\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/Group.php + + - + message: "#^Call to an undefined method \\$this\\(Winter\\\\Storm\\\\Auth\\\\Models\\\\Role\\)\\:\\:getOriginalEncryptableValues\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/Role.php + + - + message: "#^Call to an undefined method \\$this\\(Winter\\\\Storm\\\\Auth\\\\Models\\\\Role\\)\\:\\:getOriginalHashValues\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/Role.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Auth\\\\Models\\\\Role\\:\\:afterValidate\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/Role.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Auth\\\\Models\\\\Role\\:\\:beforeValidate\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/Role.php + + - + message: "#^Call to an undefined method \\$this\\(Winter\\\\Storm\\\\Auth\\\\Models\\\\User\\)\\:\\:getOriginalEncryptableValues\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/User.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Auth\\\\Models\\\\User\\:\\:afterValidate\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/User.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Auth\\\\Models\\\\User\\:\\:beforeValidate\\(\\)\\.$#" + count: 1 + path: src/Auth/Models/User.php + + - + message: "#^Parameter \\#1 \\$disk of static method Winter\\\\Storm\\\\Filesystem\\\\Filesystem\\:\\:isLocalDisk\\(\\) expects Illuminate\\\\Filesystem\\\\FilesystemAdapter, Illuminate\\\\Contracts\\\\Filesystem\\\\Filesystem given\\.$#" + count: 1 + path: src/Database/Attach/File.php + + - + message: "#^Access to an undefined property Winter\\\\Storm\\\\Database\\\\Model\\:\\:\\$purgeable\\.$#" + count: 4 + path: src/Database/Behaviors/Purgeable.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Database\\\\Model\\:\\:purgeAttributes\\(\\)\\.$#" + count: 1 + path: src/Database/Behaviors/Purgeable.php + + - + message: "#^Parameter \\#1 \\$app of class Illuminate\\\\Database\\\\DatabaseManager constructor expects Illuminate\\\\Contracts\\\\Foundation\\\\Application, Illuminate\\\\Contracts\\\\Container\\\\Container given\\.$#" + count: 1 + path: src/Database/Capsule/Manager.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\ConnectionInterface\\:\\:getName\\(\\)\\.$#" + count: 1 + path: src/Database/MemoryCache.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$is_bind\\.$#" + count: 1 + path: src/Database/Model.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$master_field\\.$#" + count: 1 + path: src/Database/Model.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$pivot_data\\.$#" + count: 1 + path: src/Database/Model.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$slave_id\\.$#" + count: 1 + path: src/Database/Model.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$slave_type\\.$#" + count: 1 + path: src/Database/Model.php + + - + message: "#^Method Winter\\\\Storm\\\\Database\\\\Model\\:\\:getDeferredBindingRecords\\(\\) should return Winter\\\\Storm\\\\Database\\\\Collection but returns Illuminate\\\\Database\\\\Eloquent\\\\Collection\\\\.$#" + count: 1 + path: src/Database/Model.php + + - + message: "#^Return type \\(Winter\\\\Storm\\\\Database\\\\Pivot\\) of method Winter\\\\Storm\\\\Database\\\\Model\\:\\:newPivot\\(\\) should be compatible with return type \\(Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Pivot\\) of method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:newPivot\\(\\)$#" + count: 1 + path: src/Database/Model.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Database\\\\Model\\:\\:errors\\(\\)\\.$#" + count: 1 + path: src/Database/ModelException.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:deleteCancel\\(\\)\\.$#" + count: 2 + path: src/Database/Models/DeferredBinding.php + + - + message: "#^Parameter \\#1 \\$haystack of function str_contains expects string, int given\\.$#" + count: 1 + path: src/Database/MorphPivot.php + + - + message: "#^Parameter \\#1 \\$ids of method Winter\\\\Storm\\\\Database\\\\Pivot\\:\\:newQueryForRestoration\\(\\) expects array\\\\|string, int given\\.$#" + count: 1 + path: src/Database/MorphPivot.php + + - + message: "#^Parameter \\#2 \\$string of function explode expects string, int given\\.$#" + count: 1 + path: src/Database/MorphPivot.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getLeftColumnName\\(\\)\\.$#" + count: 1 + path: src/Database/NestedTreeScope.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Query\\\\Builder\\:\\:\\$concats\\.$#" + count: 2 + path: src/Database/Query/Grammars/MySqlGrammar.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Query\\\\Builder\\:\\:\\$concats\\.$#" + count: 2 + path: src/Database/Query/Grammars/PostgresGrammar.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Query\\\\Builder\\:\\:\\$concats\\.$#" + count: 2 + path: src/Database/Query/Grammars/SQLiteGrammar.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Query\\\\Builder\\:\\:\\$concats\\.$#" + count: 2 + path: src/Database/Query/Grammars/SqlServerGrammar.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\ConnectionInterface\\:\\:getName\\(\\)\\.$#" + count: 1 + path: src/Database/QueryBuilder.php + + - + message: "#^Property Illuminate\\\\Database\\\\Query\\\\Builder\\:\\:\\$orders \\(array\\) does not accept null\\.$#" + count: 1 + path: src/Database/QueryBuilder.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/AttachMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/AttachMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachMany.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Database\\\\Relations\\\\AttachMany\\:\\:getRelationExistenceQueryForSelfJoin\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachMany.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/AttachOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/AttachOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachOne.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Database\\\\Relations\\\\AttachOne\\:\\:getRelationExistenceQueryForSelfJoin\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/AttachOne.php + + - + message: "#^Call to private method delete\\(\\) of parent class Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\MorphOne\\\\.$#" + count: 3 + path: src/Database/Relations/AttachOne.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/BelongsTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:bindEventOnce\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getRelationDefinition\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/BelongsTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/BelongsTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsTo.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$sessionKey\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:bindDeferred\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:bindEventOnce\\(\\)\\.$#" + count: 2 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:fireEvent\\(\\)\\.$#" + count: 4 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getRelationDefinition\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:newRelationPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:reloadRelations\\(\\)\\.$#" + count: 2 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:unbindDeferred\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Query\\\\Builder\\:\\:lists\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Database\\\\Relations\\\\BelongsToMany\\:\\:flushDuplicateCache\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Parameter \\#2 \\$columns of method Illuminate\\\\Database\\\\Eloquent\\\\Builder\\\\:\\:paginate\\(\\) expects array, int\\|null given\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Parameter \\#2 \\$currentPage \\(int\\) of method Winter\\\\Storm\\\\Database\\\\Relations\\\\BelongsToMany\\:\\:paginate\\(\\) should be compatible with parameter \\$columns \\(array\\) of method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\\\:\\:paginate\\(\\)$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Parameter \\#3 \\$columns \\(array\\) of method Winter\\\\Storm\\\\Database\\\\Relations\\\\BelongsToMany\\:\\:paginate\\(\\) should be compatible with parameter \\$pageName \\(string\\) of method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\\\:\\:paginate\\(\\)$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Parameter \\#3 \\$pageName of method Illuminate\\\\Database\\\\Eloquent\\\\Builder\\\\:\\:paginate\\(\\) expects string, array given\\.$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Parameter \\#4 \\$pageName \\(string\\) of method Winter\\\\Storm\\\\Database\\\\Relations\\\\BelongsToMany\\:\\:paginate\\(\\) should be compatible with parameter \\$page \\(int\\|null\\) of method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\\\:\\:paginate\\(\\)$#" + count: 1 + path: src/Database/Relations/BelongsToMany.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/HasMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/HasMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasMany.php + + - + message: "#^Call to private method update\\(\\) of parent class Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\HasMany\\\\.$#" + count: 1 + path: src/Database/Relations/HasMany.php + + - + message: "#^Call to private method whereNotIn\\(\\) of parent class Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\HasMany\\\\.$#" + count: 1 + path: src/Database/Relations/HasMany.php + + - + message: "#^If condition is always true\\.$#" + count: 1 + path: src/Database/Relations/HasMany.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/HasManyThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/HasManyThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasManyThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasManyThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasManyThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasManyThrough.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/HasOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/HasOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasOne.php + + - + message: "#^Call to private method update\\(\\) of parent class Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\HasOne\\\\.$#" + count: 2 + path: src/Database/Relations/HasOne.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/HasOneThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/HasOneThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasOneThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasOneThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasOneThrough.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/HasOneThrough.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/MorphMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/MorphMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphMany.php + + - + message: "#^Call to private method update\\(\\) of parent class Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\MorphMany\\\\.$#" + count: 1 + path: src/Database/Relations/MorphMany.php + + - + message: "#^Call to private method whereNotIn\\(\\) of parent class Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\MorphMany\\\\.$#" + count: 1 + path: src/Database/Relations/MorphMany.php + + - + message: "#^If condition is always true\\.$#" + count: 1 + path: src/Database/Relations/MorphMany.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/MorphOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/MorphOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphOne.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphOne.php + + - + message: "#^Call to private method update\\(\\) of parent class Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\MorphOne\\\\.$#" + count: 2 + path: src/Database/Relations/MorphOne.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/MorphTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:bindEventOnce\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/MorphTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphTo.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphTo.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\:\\:\\$countMode\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:getForeignKey\\(\\)\\.$#" + count: 3 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:select\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withDefault\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withPivot\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\Relation\\:\\:withTimestamps\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Query\\\\Builder\\:\\:lists\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Call to an undefined method Winter\\\\Storm\\\\Database\\\\Relations\\\\MorphToMany\\:\\:flushDuplicateCache\\(\\)\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Parameter \\#2 \\$columns of method Illuminate\\\\Database\\\\Eloquent\\\\Builder\\\\:\\:paginate\\(\\) expects array, int\\|null given\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Parameter \\#2 \\$currentPage \\(int\\) of method Winter\\\\Storm\\\\Database\\\\Relations\\\\MorphToMany\\:\\:paginate\\(\\) should be compatible with parameter \\$columns \\(array\\) of method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\\\:\\:paginate\\(\\)$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Parameter \\#3 \\$columns \\(array\\) of method Winter\\\\Storm\\\\Database\\\\Relations\\\\MorphToMany\\:\\:paginate\\(\\) should be compatible with parameter \\$pageName \\(string\\) of method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\\\:\\:paginate\\(\\)$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Parameter \\#3 \\$pageName of method Illuminate\\\\Database\\\\Eloquent\\\\Builder\\\\:\\:paginate\\(\\) expects string, array given\\.$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Parameter \\#4 \\$pageName \\(string\\) of method Winter\\\\Storm\\\\Database\\\\Relations\\\\MorphToMany\\:\\:paginate\\(\\) should be compatible with parameter \\$page \\(int\\|null\\) of method Illuminate\\\\Database\\\\Eloquent\\\\Relations\\\\BelongsToMany\\\\:\\:paginate\\(\\)$#" + count: 1 + path: src/Database/Relations/MorphToMany.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getSortOrderColumn\\(\\)\\.$#" + count: 1 + path: src/Database/SortableScope.php + + - + message: "#^Access to an undefined property Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:\\$children\\.$#" + count: 1 + path: src/Database/TreeCollection.php + + - + message: "#^Call to an undefined method Illuminate\\\\Database\\\\Eloquent\\\\Model\\:\\:getParentId\\(\\)\\.$#" + count: 1 + path: src/Database/TreeCollection.php + + - + message: "#^Winter\\\\Storm\\\\Extension\\\\Extendable\\:\\:extendableCall\\(\\) calls parent\\:\\:__call\\(\\) but Winter\\\\Storm\\\\Extension\\\\Extendable does not extend any class\\.$#" + count: 1 + path: src/Extension/Extendable.php + + - + message: "#^Winter\\\\Storm\\\\Extension\\\\Extendable\\:\\:extendableGet\\(\\) calls parent\\:\\:__get\\(\\) but Winter\\\\Storm\\\\Extension\\\\Extendable does not extend any class\\.$#" + count: 1 + path: src/Extension/Extendable.php + + - + message: "#^Winter\\\\Storm\\\\Extension\\\\Extendable\\:\\:extendableSet\\(\\) calls parent\\:\\:__set\\(\\) but Winter\\\\Storm\\\\Extension\\\\Extendable does not extend any class\\.$#" + count: 1 + path: src/Extension/Extendable.php + + - + message: "#^Call to an undefined method Illuminate\\\\Contracts\\\\Foundation\\\\Application\\:\\:getCachedClassesPath\\(\\)\\.$#" + count: 1 + path: src/Foundation/Console/ClearCompiledCommand.php + + - + message: "#^Parameter \\#2 \\$data \\(array\\) of method Winter\\\\Storm\\\\Mail\\\\Mailer\\:\\:queue\\(\\) should be compatible with parameter \\$queue \\(string\\|null\\) of method Illuminate\\\\Contracts\\\\Mail\\\\MailQueue\\:\\:queue\\(\\)$#" + count: 1 + path: src/Mail/Mailer.php + + - + message: "#^Parameter \\#2 \\$data \\(array\\) of method Winter\\\\Storm\\\\Mail\\\\Mailer\\:\\:queue\\(\\) should be compatible with parameter \\$queue \\(string\\|null\\) of method Illuminate\\\\Mail\\\\Mailer\\:\\:queue\\(\\)$#" + count: 1 + path: src/Mail/Mailer.php + + - + message: "#^Parameter \\#2 \\$view \\(array\\|string\\) of method Winter\\\\Storm\\\\Mail\\\\Mailer\\:\\:queueOn\\(\\) should be compatible with parameter \\$view \\(Illuminate\\\\Contracts\\\\Mail\\\\Mailable\\) of method Illuminate\\\\Mail\\\\Mailer\\:\\:queueOn\\(\\)$#" + count: 1 + path: src/Mail/Mailer.php + + - + message: "#^Parameter \\#3 \\$data \\(array\\) of method Winter\\\\Storm\\\\Mail\\\\Mailer\\:\\:later\\(\\) should be compatible with parameter \\$queue \\(string\\|null\\) of method Illuminate\\\\Contracts\\\\Mail\\\\MailQueue\\:\\:later\\(\\)$#" + count: 1 + path: src/Mail/Mailer.php + + - + message: "#^Parameter \\#3 \\$data \\(array\\) of method Winter\\\\Storm\\\\Mail\\\\Mailer\\:\\:later\\(\\) should be compatible with parameter \\$queue \\(string\\|null\\) of method Illuminate\\\\Mail\\\\Mailer\\:\\:later\\(\\)$#" + count: 1 + path: src/Mail/Mailer.php + + - + message: "#^Parameter \\#3 \\$view \\(array\\|string\\) of method Winter\\\\Storm\\\\Mail\\\\Mailer\\:\\:laterOn\\(\\) should be compatible with parameter \\$view \\(Illuminate\\\\Contracts\\\\Mail\\\\Mailable\\) of method Illuminate\\\\Mail\\\\Mailer\\:\\:laterOn\\(\\)$#" + count: 1 + path: src/Mail/Mailer.php + + - + message: "#^Parameter \\#2 \\$data \\(array\\) of method Winter\\\\Storm\\\\Support\\\\Testing\\\\Fakes\\\\MailFake\\:\\:queue\\(\\) should be compatible with parameter \\$queue \\(string\\|null\\) of method Illuminate\\\\Contracts\\\\Mail\\\\MailQueue\\:\\:queue\\(\\)$#" + count: 1 + path: src/Support/Testing/Fakes/MailFake.php + + - + message: "#^Parameter \\#2 \\$data \\(array\\) of method Winter\\\\Storm\\\\Support\\\\Testing\\\\Fakes\\\\MailFake\\:\\:queue\\(\\) should be compatible with parameter \\$queue \\(string\\|null\\) of method Illuminate\\\\Support\\\\Testing\\\\Fakes\\\\MailFake\\:\\:queue\\(\\)$#" + count: 1 + path: src/Support/Testing/Fakes/MailFake.php + + - + message: "#^Return type \\(void\\) of method Winter\\\\Storm\\\\Support\\\\Testing\\\\Fakes\\\\MailFake\\:\\:send\\(\\) should be compatible with return type \\(Illuminate\\\\Mail\\\\SentMessage\\|null\\) of method Illuminate\\\\Contracts\\\\Mail\\\\Mailer\\:\\:send\\(\\)$#" + count: 1 + path: src/Support/Testing/Fakes/MailFake.php + + - + message: "#^Call to function is_null\\(\\) with Closure will always evaluate to false\\.$#" + count: 1 + path: src/Validation/Factory.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..d36bb09c3 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,15 @@ +includes: + - ./vendor/nunomaduro/larastan/extension.neon + - phpstan-baseline.neon + +parameters: + paths: + - src + level: 5 + excludePaths: + # Exclude PHP Parser files + - src/Parse/PHP/ArrayFile.php + - src/Parse/PHP/ArrayPrinter.php + databaseMigrationsPath: + - src/Auth/Migrations + - src/Database/Migrations diff --git a/src/Argon/Argon.php b/src/Argon/Argon.php index 843485006..a4cff3666 100644 --- a/src/Argon/Argon.php +++ b/src/Argon/Argon.php @@ -1,11 +1,11 @@ - */ -class AssetCache implements AssetInterface -{ - private $asset; - private $cache; - - public function __construct(AssetInterface $asset, CacheInterface $cache) - { - $this->asset = $asset; - $this->cache = $cache; - } - - public function ensureFilter(FilterInterface $filter) - { - $this->asset->ensureFilter($filter); - } - - public function getFilters() - { - return $this->asset->getFilters(); - } - - public function clearFilters() - { - $this->asset->clearFilters(); - } - - public function load(FilterInterface $additionalFilter = null) - { - $cacheKey = self::getCacheKey($this->asset, $additionalFilter, 'load'); - if ($this->cache->has($cacheKey)) { - $this->asset->setContent($this->cache->get($cacheKey)); - - return; - } - - $this->asset->load($additionalFilter); - $this->cache->set($cacheKey, $this->asset->getContent()); - } - - public function dump(FilterInterface $additionalFilter = null) - { - $cacheKey = self::getCacheKey($this->asset, $additionalFilter, 'dump'); - if ($this->cache->has($cacheKey)) { - return $this->cache->get($cacheKey); - } - - $content = $this->asset->dump($additionalFilter); - $this->cache->set($cacheKey, $content); - - return $content; - } - - public function getContent() - { - return $this->asset->getContent(); - } - - public function setContent($content) - { - $this->asset->setContent($content); - } - - public function getSourceRoot() - { - return $this->asset->getSourceRoot(); - } - - public function getSourcePath() - { - return $this->asset->getSourcePath(); - } - - public function getSourceDirectory() - { - return $this->asset->getSourceDirectory(); - } - - public function getTargetPath() - { - return $this->asset->getTargetPath(); - } - - public function setTargetPath($targetPath) - { - $this->asset->setTargetPath($targetPath); - } - - public function getLastModified() - { - return $this->asset->getLastModified(); - } - - public function getVars() - { - return $this->asset->getVars(); - } - - public function setValues(array $values) - { - $this->asset->setValues($values); - } - - public function getValues() - { - return $this->asset->getValues(); - } - - /** - * Returns a cache key for the current asset. - * - * The key is composed of everything but an asset's content: - * - * * source root - * * source path - * * target url - * * last modified - * * filters - * - * @param AssetInterface $asset The asset - * @param FilterInterface $additionalFilter Any additional filter being applied - * @param string $salt Salt for the key - * - * @return string A key for identifying the current asset - */ - private static function getCacheKey(AssetInterface $asset, FilterInterface $additionalFilter = null, $salt = '') - { - if ($additionalFilter) { - $asset = clone $asset; - $asset->ensureFilter($additionalFilter); - } - - $cacheKey = $asset->getSourceRoot(); - $cacheKey .= $asset->getSourcePath(); - $cacheKey .= $asset->getTargetPath(); - $cacheKey .= $asset->getLastModified(); - - foreach ($asset->getFilters() as $filter) { - if ($filter instanceof HashableInterface) { - $cacheKey .= $filter->hash(); - } else { - $cacheKey .= serialize($filter); - } - } - - if ($values = $asset->getValues()) { - asort($values); - $cacheKey .= serialize($values); - } - - return md5($cacheKey.$salt); - } -} diff --git a/src/Assetic/Asset/AssetCollection.php b/src/Assetic/Asset/AssetCollection.php deleted file mode 100644 index ced5be74a..000000000 --- a/src/Assetic/Asset/AssetCollection.php +++ /dev/null @@ -1,236 +0,0 @@ - - */ -class AssetCollection implements \IteratorAggregate, AssetCollectionInterface -{ - private $assets; - private $filters; - private $sourceRoot; - private $targetPath; - private $content; - private $clones; - private $vars; - private $values; - - /** - * Constructor. - * - * @param array $assets Assets for the current collection - * @param array $filters Filters for the current collection - * @param string $sourceRoot The root directory - * @param array $vars - */ - public function __construct($assets = array(), $filters = array(), $sourceRoot = null, array $vars = array()) - { - $this->assets = array(); - foreach ($assets as $asset) { - $this->add($asset); - } - - $this->filters = new FilterCollection($filters); - $this->sourceRoot = $sourceRoot; - $this->clones = new \SplObjectStorage(); - $this->vars = $vars; - $this->values = array(); - } - - public function __clone() - { - $this->filters = clone $this->filters; - $this->clones = new \SplObjectStorage(); - } - - public function all() - { - return $this->assets; - } - - public function add(AssetInterface $asset) - { - $this->assets[] = $asset; - } - - public function removeLeaf(AssetInterface $needle, $graceful = false) - { - foreach ($this->assets as $i => $asset) { - $clone = isset($this->clones[$asset]) ? $this->clones[$asset] : null; - if (in_array($needle, array($asset, $clone), true)) { - unset($this->clones[$asset], $this->assets[$i]); - - return true; - } - - if ($asset instanceof AssetCollectionInterface && $asset->removeLeaf($needle, true)) { - return true; - } - } - - if ($graceful) { - return false; - } - - throw new \InvalidArgumentException('Leaf not found.'); - } - - public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, $graceful = false) - { - foreach ($this->assets as $i => $asset) { - $clone = isset($this->clones[$asset]) ? $this->clones[$asset] : null; - if (in_array($needle, array($asset, $clone), true)) { - unset($this->clones[$asset]); - $this->assets[$i] = $replacement; - - return true; - } - - if ($asset instanceof AssetCollectionInterface && $asset->replaceLeaf($needle, $replacement, true)) { - return true; - } - } - - if ($graceful) { - return false; - } - - throw new \InvalidArgumentException('Leaf not found.'); - } - - public function ensureFilter(FilterInterface $filter) - { - $this->filters->ensure($filter); - } - - public function getFilters() - { - return $this->filters->all(); - } - - public function clearFilters() - { - $this->filters->clear(); - $this->clones = new \SplObjectStorage(); - } - - public function load(FilterInterface $additionalFilter = null) - { - // loop through leaves and load each asset - $parts = array(); - foreach ($this as $asset) { - $asset->load($additionalFilter); - $parts[] = $asset->getContent(); - } - - $this->content = implode("\n", $parts); - } - - public function dump(FilterInterface $additionalFilter = null) - { - // loop through leaves and dump each asset - $parts = array(); - foreach ($this as $asset) { - $parts[] = $asset->dump($additionalFilter); - } - - return implode("\n", $parts); - } - - public function getContent() - { - return $this->content; - } - - public function setContent($content) - { - $this->content = $content; - } - - public function getSourceRoot() - { - return $this->sourceRoot; - } - - public function getSourcePath() - { - } - - public function getSourceDirectory() - { - } - - public function getTargetPath() - { - return $this->targetPath; - } - - public function setTargetPath($targetPath) - { - $this->targetPath = $targetPath; - } - - /** - * Returns the highest last-modified value of all assets in the current collection. - * - * @return integer|null A UNIX timestamp - */ - public function getLastModified() - { - if (!count($this->assets)) { - return; - } - - $mtime = 0; - foreach ($this as $asset) { - $assetMtime = $asset->getLastModified(); - if ($assetMtime > $mtime) { - $mtime = $assetMtime; - } - } - - return $mtime; - } - - /** - * Returns an iterator for looping recursively over unique leaves. - */ - public function getIterator() - { - return new \RecursiveIteratorIterator(new AssetCollectionFilterIterator(new AssetCollectionIterator($this, $this->clones))); - } - - public function getVars() - { - return $this->vars; - } - - public function setValues(array $values) - { - $this->values = $values; - - foreach ($this as $asset) { - $asset->setValues(array_intersect_key($values, array_flip($asset->getVars()))); - } - } - - public function getValues() - { - return $this->values; - } -} diff --git a/src/Assetic/Asset/AssetCollectionInterface.php b/src/Assetic/Asset/AssetCollectionInterface.php deleted file mode 100644 index 0bdb5bb5d..000000000 --- a/src/Assetic/Asset/AssetCollectionInterface.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ -interface AssetCollectionInterface extends AssetInterface, \Traversable -{ - /** - * Returns all child assets. - * - * @return array An array of AssetInterface objects - */ - public function all(); - - /** - * Adds an asset to the current collection. - * - * @param AssetInterface $asset An asset - */ - public function add(AssetInterface $asset); - - /** - * Removes a leaf. - * - * @param AssetInterface $leaf The leaf to remove - * @param Boolean $graceful Whether the failure should return false or throw an exception - * - * @return Boolean Whether the asset has been found - * - * @throws \InvalidArgumentException If the asset cannot be found - */ - public function removeLeaf(AssetInterface $leaf, $graceful = false); - - /** - * Replaces an existing leaf with a new one. - * - * @param AssetInterface $needle The current asset to replace - * @param AssetInterface $replacement The new asset - * @param Boolean $graceful Whether the failure should return false or throw an exception - * - * @return Boolean Whether the asset has been found - * - * @throws \InvalidArgumentException If the asset cannot be found - */ - public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, $graceful = false); -} diff --git a/src/Assetic/Asset/AssetInterface.php b/src/Assetic/Asset/AssetInterface.php deleted file mode 100644 index e6a364a2c..000000000 --- a/src/Assetic/Asset/AssetInterface.php +++ /dev/null @@ -1,164 +0,0 @@ - - */ -interface AssetInterface -{ - /** - * Ensures the current asset includes the supplied filter. - * - * @param FilterInterface $filter A filter - */ - public function ensureFilter(FilterInterface $filter); - - /** - * Returns an array of filters currently applied. - * - * @return array An array of filters - */ - public function getFilters(); - - /** - * Clears all filters from the current asset. - */ - public function clearFilters(); - - /** - * Loads the asset into memory and applies load filters. - * - * You may provide an additional filter to apply during load. - * - * @param FilterInterface $additionalFilter An additional filter - */ - public function load(FilterInterface $additionalFilter = null); - - /** - * Applies dump filters and returns the asset as a string. - * - * You may provide an additional filter to apply during dump. - * - * Dumping an asset should not change its state. - * - * If the current asset has not been loaded yet, it should be - * automatically loaded at this time. - * - * @param FilterInterface $additionalFilter An additional filter - * - * @return string The filtered content of the current asset - */ - public function dump(FilterInterface $additionalFilter = null); - - /** - * Returns the loaded content of the current asset. - * - * @return string The content - */ - public function getContent(); - - /** - * Sets the content of the current asset. - * - * Filters can use this method to change the content of the asset. - * - * @param string $content The asset content - */ - public function setContent($content); - - /** - * Returns an absolute path or URL to the source asset's root directory. - * - * This value should be an absolute path to a directory in the filesystem, - * an absolute URL with no path, or null. - * - * For example: - * - * * '/path/to/web' - * * 'http://example.com' - * * null - * - * @return string|null The asset's root - */ - public function getSourceRoot(); - - /** - * Returns the relative path for the source asset. - * - * This value can be combined with the asset's source root (if both are - * non-null) to get something compatible with file_get_contents(). - * - * For example: - * - * * 'js/main.js' - * * 'main.js' - * * null - * - * @return string|null The source asset path - */ - public function getSourcePath(); - - /** - * Returns the asset's source directory. - * - * The source directory is the directory the asset was located in - * and can be used to resolve references relative to an asset. - * - * @return string|null The asset's source directory - */ - public function getSourceDirectory(); - - /** - * Returns the URL for the current asset. - * - * @return string|null A web URL where the asset will be dumped - */ - public function getTargetPath(); - - /** - * Sets the URL for the current asset. - * - * @param string $targetPath A web URL where the asset will be dumped - */ - public function setTargetPath($targetPath); - - /** - * Returns the time the current asset was last modified. - * - * @return integer|null A UNIX timestamp - */ - public function getLastModified(); - - /** - * Returns an array of variable names for this asset. - * - * @return array - */ - public function getVars(); - - /** - * Sets the values for the asset's variables. - * - * @param array $values - */ - public function setValues(array $values); - - /** - * Returns the current values for this asset. - * - * @return array an array of strings - */ - public function getValues(); -} diff --git a/src/Assetic/Asset/AssetReference.php b/src/Assetic/Asset/AssetReference.php deleted file mode 100644 index 07ecedb03..000000000 --- a/src/Assetic/Asset/AssetReference.php +++ /dev/null @@ -1,162 +0,0 @@ - - */ -class AssetReference implements AssetInterface -{ - private $am; - private $name; - private $filters = array(); - private $clone = false; - private $asset; - - public function __construct(AssetManager $am, $name) - { - $this->am = $am; - $this->name = $name; - } - - public function __clone() - { - $this->clone = true; - - if ($this->asset) { - $this->asset = clone $this->asset; - } - } - - public function ensureFilter(FilterInterface $filter) - { - $this->filters[] = $filter; - } - - public function getFilters() - { - $this->flushFilters(); - - return $this->callAsset(__FUNCTION__); - } - - public function clearFilters() - { - $this->filters = array(); - $this->callAsset(__FUNCTION__); - } - - public function load(FilterInterface $additionalFilter = null) - { - $this->flushFilters(); - - return $this->callAsset(__FUNCTION__, array($additionalFilter)); - } - - public function dump(FilterInterface $additionalFilter = null) - { - $this->flushFilters(); - - return $this->callAsset(__FUNCTION__, array($additionalFilter)); - } - - public function getContent() - { - return $this->callAsset(__FUNCTION__); - } - - public function setContent($content) - { - $this->callAsset(__FUNCTION__, array($content)); - } - - public function getSourceRoot() - { - return $this->callAsset(__FUNCTION__); - } - - public function getSourcePath() - { - return $this->callAsset(__FUNCTION__); - } - - public function getSourceDirectory() - { - return $this->callAsset(__FUNCTION__); - } - - public function getTargetPath() - { - return $this->callAsset(__FUNCTION__); - } - - public function setTargetPath($targetPath) - { - $this->callAsset(__FUNCTION__, array($targetPath)); - } - - public function getLastModified() - { - return $this->callAsset(__FUNCTION__); - } - - public function getVars() - { - return $this->callAsset(__FUNCTION__); - } - - public function getValues() - { - return $this->callAsset(__FUNCTION__); - } - - public function setValues(array $values) - { - $this->callAsset(__FUNCTION__, array($values)); - } - - // private - - private function callAsset($method, $arguments = array()) - { - $asset = $this->resolve(); - - return call_user_func_array(array($asset, $method), $arguments); - } - - private function flushFilters() - { - $asset = $this->resolve(); - - while ($filter = array_shift($this->filters)) { - $asset->ensureFilter($filter); - } - } - - private function resolve() - { - if ($this->asset) { - return $this->asset; - } - - $asset = $this->am->get($this->name); - - if ($this->clone) { - $asset = $this->asset = clone $asset; - } - - return $asset; - } -} diff --git a/src/Assetic/Asset/BaseAsset.php b/src/Assetic/Asset/BaseAsset.php deleted file mode 100644 index 7d799e1b2..000000000 --- a/src/Assetic/Asset/BaseAsset.php +++ /dev/null @@ -1,179 +0,0 @@ - - */ -abstract class BaseAsset implements AssetInterface -{ - private $filters; - private $sourceRoot; - private $sourcePath; - private $sourceDir; - private $targetPath; - private $content; - private $loaded; - private $vars; - private $values; - - /** - * Constructor. - * - * @param array $filters Filters for the asset - * @param string $sourceRoot The root directory - * @param string $sourcePath The asset path - * @param array $vars - */ - public function __construct($filters = array(), $sourceRoot = null, $sourcePath = null, array $vars = array()) - { - $this->filters = new FilterCollection($filters); - $this->sourceRoot = $sourceRoot; - $this->sourcePath = $sourcePath; - if ($sourcePath && $sourceRoot) { - $this->sourceDir = dirname("$sourceRoot/$sourcePath"); - } - $this->vars = $vars; - $this->values = array(); - $this->loaded = false; - } - - public function __clone() - { - $this->filters = clone $this->filters; - } - - public function ensureFilter(FilterInterface $filter) - { - $this->filters->ensure($filter); - } - - public function getFilters() - { - return $this->filters->all(); - } - - public function clearFilters() - { - $this->filters->clear(); - } - - /** - * Encapsulates asset loading logic. - * - * @param string $content The asset content - * @param FilterInterface $additionalFilter An additional filter - */ - protected function doLoad($content, FilterInterface $additionalFilter = null) - { - $filter = clone $this->filters; - if ($additionalFilter) { - $filter->ensure($additionalFilter); - } - - $asset = clone $this; - $asset->setContent($content); - - $filter->filterLoad($asset); - $this->content = $asset->getContent(); - - $this->loaded = true; - } - - public function dump(FilterInterface $additionalFilter = null) - { - if (!$this->loaded) { - $this->load(); - } - - $filter = clone $this->filters; - if ($additionalFilter) { - $filter->ensure($additionalFilter); - } - - $asset = clone $this; - $filter->filterDump($asset); - - return $asset->getContent(); - } - - public function getContent() - { - return $this->content; - } - - public function setContent($content) - { - $this->content = $content; - } - - public function getSourceRoot() - { - return $this->sourceRoot; - } - - public function getSourcePath() - { - return $this->sourcePath; - } - - public function getSourceDirectory() - { - return $this->sourceDir; - } - - public function getTargetPath() - { - return $this->targetPath; - } - - public function setTargetPath($targetPath) - { - if ($this->vars) { - foreach ($this->vars as $var) { - if (false === strpos($targetPath, $var)) { - throw new \RuntimeException(sprintf('The asset target path "%s" must contain the variable "{%s}".', $targetPath, $var)); - } - } - } - - $this->targetPath = $targetPath; - } - - public function getVars() - { - return $this->vars; - } - - public function setValues(array $values) - { - foreach ($values as $var => $v) { - if (!in_array($var, $this->vars, true)) { - throw new \InvalidArgumentException(sprintf('The asset with source path "%s" has no variable named "%s".', $this->sourcePath, $var)); - } - } - - $this->values = $values; - $this->loaded = false; - } - - public function getValues() - { - return $this->values; - } -} diff --git a/src/Assetic/Asset/FileAsset.php b/src/Assetic/Asset/FileAsset.php deleted file mode 100644 index c26c5efa1..000000000 --- a/src/Assetic/Asset/FileAsset.php +++ /dev/null @@ -1,76 +0,0 @@ - - */ -class FileAsset extends BaseAsset -{ - private $source; - - /** - * Constructor. - * - * @param string $source An absolute path - * @param array $filters An array of filters - * @param string $sourceRoot The source asset root directory - * @param string $sourcePath The source asset path - * @param array $vars - * - * @throws \InvalidArgumentException If the supplied root doesn't match the source when guessing the path - */ - public function __construct($source, $filters = array(), $sourceRoot = null, $sourcePath = null, array $vars = array()) - { - if (null === $sourceRoot) { - $sourceRoot = dirname($source); - if (null === $sourcePath) { - $sourcePath = basename($source); - } - } elseif (null === $sourcePath) { - if (0 !== strpos($source, $sourceRoot)) { - throw new \InvalidArgumentException(sprintf('The source "%s" is not in the root directory "%s"', $source, $sourceRoot)); - } - - $sourcePath = substr($source, strlen($sourceRoot) + 1); - } - - $this->source = $source; - - parent::__construct($filters, $sourceRoot, $sourcePath, $vars); - } - - public function load(FilterInterface $additionalFilter = null) - { - $source = VarUtils::resolve($this->source, $this->getVars(), $this->getValues()); - - if (!is_file($source)) { - throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $source)); - } - - $this->doLoad(file_get_contents($source), $additionalFilter); - } - - public function getLastModified() - { - $source = VarUtils::resolve($this->source, $this->getVars(), $this->getValues()); - - if (!is_file($source)) { - throw new \RuntimeException(sprintf('The source file "%s" does not exist.', $source)); - } - - return filemtime($source); - } -} diff --git a/src/Assetic/Asset/GlobAsset.php b/src/Assetic/Asset/GlobAsset.php deleted file mode 100644 index b465b4f50..000000000 --- a/src/Assetic/Asset/GlobAsset.php +++ /dev/null @@ -1,113 +0,0 @@ - - */ -class GlobAsset extends AssetCollection -{ - private $globs; - private $initialized; - - /** - * Constructor. - * - * @param string|array $globs A single glob path or array of paths - * @param array $filters An array of filters - * @param string $root The root directory - * @param array $vars - */ - public function __construct($globs, $filters = array(), $root = null, array $vars = array()) - { - $this->globs = (array) $globs; - $this->initialized = false; - - parent::__construct(array(), $filters, $root, $vars); - } - - public function all() - { - if (!$this->initialized) { - $this->initialize(); - } - - return parent::all(); - } - - public function load(FilterInterface $additionalFilter = null) - { - if (!$this->initialized) { - $this->initialize(); - } - - parent::load($additionalFilter); - } - - public function dump(FilterInterface $additionalFilter = null) - { - if (!$this->initialized) { - $this->initialize(); - } - - return parent::dump($additionalFilter); - } - - public function getLastModified() - { - if (!$this->initialized) { - $this->initialize(); - } - - return parent::getLastModified(); - } - - public function getIterator() - { - if (!$this->initialized) { - $this->initialize(); - } - - return parent::getIterator(); - } - - public function setValues(array $values) - { - parent::setValues($values); - $this->initialized = false; - } - - /** - * Initializes the collection based on the glob(s) passed in. - */ - private function initialize() - { - foreach ($this->globs as $glob) { - $glob = VarUtils::resolve($glob, $this->getVars(), $this->getValues()); - - if (false !== $paths = glob($glob)) { - foreach ($paths as $path) { - if (is_file($path)) { - $asset = new FileAsset($path, array(), $this->getSourceRoot(), null, $this->getVars()); - $asset->setValues($this->getValues()); - $this->add($asset); - } - } - } - } - - $this->initialized = true; - } -} diff --git a/src/Assetic/Asset/HttpAsset.php b/src/Assetic/Asset/HttpAsset.php deleted file mode 100644 index fabfe453c..000000000 --- a/src/Assetic/Asset/HttpAsset.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ -class HttpAsset extends BaseAsset -{ - private $sourceUrl; - private $ignoreErrors; - - /** - * Constructor. - * - * @param string $sourceUrl The source URL - * @param array $filters An array of filters - * @param Boolean $ignoreErrors - * @param array $vars - * - * @throws \InvalidArgumentException If the first argument is not an URL - */ - public function __construct($sourceUrl, $filters = array(), $ignoreErrors = false, array $vars = array()) - { - if (0 === strpos($sourceUrl, '//')) { - $sourceUrl = 'http:'.$sourceUrl; - } elseif (false === strpos($sourceUrl, '://')) { - throw new \InvalidArgumentException(sprintf('"%s" is not a valid URL.', $sourceUrl)); - } - - $this->sourceUrl = $sourceUrl; - $this->ignoreErrors = $ignoreErrors; - - list($scheme, $url) = explode('://', $sourceUrl, 2); - list($host, $path) = explode('/', $url, 2); - - parent::__construct($filters, $scheme.'://'.$host, $path, $vars); - } - - public function load(FilterInterface $additionalFilter = null) - { - $content = @file_get_contents( - VarUtils::resolve($this->sourceUrl, $this->getVars(), $this->getValues()) - ); - - if (false === $content && !$this->ignoreErrors) { - throw new \RuntimeException(sprintf('Unable to load asset from URL "%s"', $this->sourceUrl)); - } - - $this->doLoad($content, $additionalFilter); - } - - public function getLastModified() - { - if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(array('http' => array('method' => 'HEAD'))))) { - foreach ($http_response_header as $header) { - if (0 === stripos($header, 'Last-Modified: ')) { - list(, $mtime) = explode(':', $header, 2); - - return strtotime(trim($mtime)); - } - } - } - } -} diff --git a/src/Assetic/Asset/Iterator/AssetCollectionFilterIterator.php b/src/Assetic/Asset/Iterator/AssetCollectionFilterIterator.php deleted file mode 100644 index c2ce7f7ae..000000000 --- a/src/Assetic/Asset/Iterator/AssetCollectionFilterIterator.php +++ /dev/null @@ -1,82 +0,0 @@ - - */ -class AssetCollectionFilterIterator extends \RecursiveFilterIterator -{ - private $visited; - private $sources; - - /** - * Constructor. - * - * @param AssetCollectionIterator $iterator The inner iterator - * @param array $visited An array of visited asset objects - * @param array $sources An array of visited source strings - */ - public function __construct(AssetCollectionIterator $iterator, array $visited = array(), array $sources = array()) - { - parent::__construct($iterator); - - $this->visited = $visited; - $this->sources = $sources; - } - - /** - * Determines whether the current asset is a duplicate. - * - * De-duplication is performed based on either strict equality or by - * matching sources. - * - * @return Boolean Returns true if we have not seen this asset yet - */ - public function accept() - { - $asset = $this->getInnerIterator()->current(true); - $duplicate = false; - - // check strict equality - if (in_array($asset, $this->visited, true)) { - $duplicate = true; - } else { - $this->visited[] = $asset; - } - - // check source - $sourceRoot = $asset->getSourceRoot(); - $sourcePath = $asset->getSourcePath(); - if ($sourceRoot && $sourcePath) { - $source = $sourceRoot.'/'.$sourcePath; - if (in_array($source, $this->sources)) { - $duplicate = true; - } else { - $this->sources[] = $source; - } - } - - return !$duplicate; - } - - /** - * Passes visited objects and source URLs to the child iterator. - */ - public function getChildren() - { - return new self($this->getInnerIterator()->getChildren(), $this->visited, $this->sources); - } -} diff --git a/src/Assetic/Asset/Iterator/AssetCollectionIterator.php b/src/Assetic/Asset/Iterator/AssetCollectionIterator.php deleted file mode 100644 index faf6b1605..000000000 --- a/src/Assetic/Asset/Iterator/AssetCollectionIterator.php +++ /dev/null @@ -1,126 +0,0 @@ - - */ -class AssetCollectionIterator implements \RecursiveIterator -{ - private $assets; - private $filters; - private $vars; - private $output; - private $clones; - - public function __construct(AssetCollectionInterface $coll, \SplObjectStorage $clones) - { - $this->assets = $coll->all(); - $this->filters = $coll->getFilters(); - $this->vars = $coll->getVars(); - $this->output = $coll->getTargetPath(); - $this->clones = $clones; - - if (false === $pos = strrpos($this->output, '.')) { - $this->output .= '_*'; - } else { - $this->output = substr($this->output, 0, $pos).'_*'.substr($this->output, $pos); - } - } - - /** - * Returns a copy of the current asset with filters and a target URL applied. - * - * @param Boolean $raw Returns the unmodified asset if true - * - * @return \Assetic\Asset\AssetInterface - */ - public function current($raw = false) - { - $asset = current($this->assets); - - if ($raw) { - return $asset; - } - - // clone once - if (!isset($this->clones[$asset])) { - $clone = $this->clones[$asset] = clone $asset; - - // generate a target path based on asset name - $name = sprintf('%s_%d', pathinfo($asset->getSourcePath(), PATHINFO_FILENAME) ?: 'part', $this->key() + 1); - - $name = $this->removeDuplicateVar($name); - - $clone->setTargetPath(str_replace('*', $name, $this->output)); - } else { - $clone = $this->clones[$asset]; - } - - // cascade filters - foreach ($this->filters as $filter) { - $clone->ensureFilter($filter); - } - - return $clone; - } - - public function key() - { - return key($this->assets); - } - - public function next() - { - return next($this->assets); - } - - public function rewind() - { - return reset($this->assets); - } - - public function valid() - { - return false !== current($this->assets); - } - - public function hasChildren() - { - return current($this->assets) instanceof AssetCollectionInterface; - } - - /** - * @uses current() - */ - public function getChildren() - { - return new self($this->current(), $this->clones); - } - - private function removeDuplicateVar($name) - { - foreach ($this->vars as $var) { - $var = '{'.$var.'}'; - if (false !== strpos($name, $var) && false !== strpos($this->output, $var)) { - $name = str_replace($var, '', $name); - } - } - - return $name; - } -} diff --git a/src/Assetic/Asset/StringAsset.php b/src/Assetic/Asset/StringAsset.php deleted file mode 100644 index a047e2c1b..000000000 --- a/src/Assetic/Asset/StringAsset.php +++ /dev/null @@ -1,53 +0,0 @@ - - */ -class StringAsset extends BaseAsset -{ - private $string; - private $lastModified; - - /** - * Constructor. - * - * @param string $content The content of the asset - * @param array $filters Filters for the asset - * @param string $sourceRoot The source asset root directory - * @param string $sourcePath The source asset path - */ - public function __construct($content, $filters = array(), $sourceRoot = null, $sourcePath = null) - { - $this->string = $content; - - parent::__construct($filters, $sourceRoot, $sourcePath); - } - - public function load(FilterInterface $additionalFilter = null) - { - $this->doLoad($this->string, $additionalFilter); - } - - public function setLastModified($lastModified) - { - $this->lastModified = $lastModified; - } - - public function getLastModified() - { - return $this->lastModified; - } -} diff --git a/src/Assetic/AssetManager.php b/src/Assetic/AssetManager.php deleted file mode 100644 index 1dfb66d6e..000000000 --- a/src/Assetic/AssetManager.php +++ /dev/null @@ -1,87 +0,0 @@ - - */ -class AssetManager -{ - private $assets = array(); - - /** - * Gets an asset by name. - * - * @param string $name The asset name - * - * @return AssetInterface The asset - * - * @throws \InvalidArgumentException If there is no asset by that name - */ - public function get($name) - { - if (!isset($this->assets[$name])) { - throw new \InvalidArgumentException(sprintf('There is no "%s" asset.', $name)); - } - - return $this->assets[$name]; - } - - /** - * Checks if the current asset manager has a certain asset. - * - * @param string $name an asset name - * - * @return Boolean True if the asset has been set, false if not - */ - public function has($name) - { - return isset($this->assets[$name]); - } - - /** - * Registers an asset to the current asset manager. - * - * @param string $name The asset name - * @param AssetInterface $asset The asset - * - * @throws \InvalidArgumentException If the asset name is invalid - */ - public function set($name, AssetInterface $asset) - { - if (!ctype_alnum(str_replace('_', '', $name))) { - throw new \InvalidArgumentException(sprintf('The name "%s" is invalid.', $name)); - } - - $this->assets[$name] = $asset; - } - - /** - * Returns an array of asset names. - * - * @return array An array of asset names - */ - public function getNames() - { - return array_keys($this->assets); - } - - /** - * Clears all assets. - */ - public function clear() - { - $this->assets = array(); - } -} diff --git a/src/Assetic/AssetWriter.php b/src/Assetic/AssetWriter.php deleted file mode 100644 index 89cb69896..000000000 --- a/src/Assetic/AssetWriter.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @author Johannes M. Schmitt - */ -class AssetWriter -{ - private $dir; - private $values; - - /** - * Constructor. - * - * @param string $dir The base web directory - * @param array $values Variable values - * - * @throws \InvalidArgumentException if a variable value is not a string - */ - public function __construct($dir, array $values = array()) - { - foreach ($values as $var => $vals) { - foreach ($vals as $value) { - if (!is_string($value)) { - throw new \InvalidArgumentException(sprintf('All variable values must be strings, but got %s for variable "%s".', json_encode($value), $var)); - } - } - } - - $this->dir = $dir; - $this->values = $values; - } - - public function writeManagerAssets(AssetManager $am) - { - foreach ($am->getNames() as $name) { - $this->writeAsset($am->get($name)); - } - } - - public function writeAsset(AssetInterface $asset) - { - foreach (VarUtils::getCombinations($asset->getVars(), $this->values) as $combination) { - $asset->setValues($combination); - - static::write( - $this->dir.'/'.VarUtils::resolve( - $asset->getTargetPath(), - $asset->getVars(), - $asset->getValues() - ), - $asset->dump() - ); - } - } - - protected static function write($path, $contents) - { - if (!is_dir($dir = dirname($path)) && false === @mkdir($dir, 0777, true)) { - throw new \RuntimeException('Unable to create directory '.$dir); - } - - if (false === @file_put_contents($path, $contents)) { - throw new \RuntimeException('Unable to write file '.$path); - } - } - - /** - * Not used. - * - * This method is provided for backward compatibility with certain versions - * of AsseticBundle. - */ - private function getCombinations(array $vars) - { - return VarUtils::getCombinations($vars, $this->values); - } -} diff --git a/src/Assetic/Cache/ApcCache.php b/src/Assetic/Cache/ApcCache.php deleted file mode 100644 index 71646b92e..000000000 --- a/src/Assetic/Cache/ApcCache.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -class ApcCache implements CacheInterface -{ - public $ttl = 0; - - /** - * @see CacheInterface::has() - */ - public function has($key) - { - return apc_exists($key); - } - - /** - * @see CacheInterface::get() - */ - public function get($key) - { - $value = apc_fetch($key, $success); - - if (!$success) { - throw new \RuntimeException('There is no cached value for '.$key); - } - - return $value; - } - - /** - * @see CacheInterface::set() - */ - public function set($key, $value) - { - $store = apc_store($key, $value, $this->ttl); - - if (!$store) { - throw new \RuntimeException('Unable to store "'.$key.'" for '.$this->ttl.' seconds.'); - } - - return $store; - } - - /** - * @see CacheInterface::remove() - */ - public function remove($key) - { - return apc_delete($key); - } -} diff --git a/src/Assetic/Cache/ArrayCache.php b/src/Assetic/Cache/ArrayCache.php deleted file mode 100644 index 8aaccd14b..000000000 --- a/src/Assetic/Cache/ArrayCache.php +++ /dev/null @@ -1,56 +0,0 @@ - - */ -class ArrayCache implements CacheInterface -{ - private $cache = array(); - - /** - * @see CacheInterface::has() - */ - public function has($key) - { - return isset($this->cache[$key]); - } - - /** - * @see CacheInterface::get() - */ - public function get($key) - { - if (!$this->has($key)) { - throw new \RuntimeException('There is no cached value for '.$key); - } - - return $this->cache[$key]; - } - - /** - * @see CacheInterface::set() - */ - public function set($key, $value) - { - $this->cache[$key] = $value; - } - - /** - * @see CacheInterface::remove() - */ - public function remove($key) - { - unset($this->cache[$key]); - } -} diff --git a/src/Assetic/Cache/CacheInterface.php b/src/Assetic/Cache/CacheInterface.php deleted file mode 100644 index 6cc2008e4..000000000 --- a/src/Assetic/Cache/CacheInterface.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -interface CacheInterface -{ - /** - * Checks if the cache has a value for a key. - * - * @param string $key A unique key - * - * @return Boolean Whether the cache has a value for this key - */ - public function has($key); - - /** - * Returns the value for a key. - * - * @param string $key A unique key - * - * @return string|null The value in the cache - */ - public function get($key); - - /** - * Sets a value in the cache. - * - * @param string $key A unique key - * @param string $value The value to cache - */ - public function set($key, $value); - - /** - * Removes a value from the cache. - * - * @param string $key A unique key - */ - public function remove($key); -} diff --git a/src/Assetic/Cache/ConfigCache.php b/src/Assetic/Cache/ConfigCache.php deleted file mode 100644 index 1123f90a9..000000000 --- a/src/Assetic/Cache/ConfigCache.php +++ /dev/null @@ -1,121 +0,0 @@ - - */ -class ConfigCache -{ - private $dir; - - /** - * Construct. - * - * @param string $dir The cache directory - */ - public function __construct($dir) - { - $this->dir = $dir; - } - - /** - * Checks of the cache has a file. - * - * @param string $resource A cache key - * - * @return Boolean True if a file exists - */ - public function has($resource) - { - return file_exists($this->getSourcePath($resource)); - } - - /** - * Writes a value to a file. - * - * @param string $resource A cache key - * @param mixed $value A value to cache - */ - public function set($resource, $value) - { - $path = $this->getSourcePath($resource); - - if (!is_dir($dir = dirname($path)) && false === @mkdir($dir, 0777, true)) { - // @codeCoverageIgnoreStart - throw new \RuntimeException('Unable to create directory '.$dir); - // @codeCoverageIgnoreEnd - } - - if (false === @file_put_contents($path, sprintf("getSourcePath($resource); - - if (!file_exists($path)) { - throw new \RuntimeException('There is no cached value for '.$resource); - } - - return include $path; - } - - /** - * Returns a timestamp for when the cache was created. - * - * @param string $resource A cache key - * - * @return integer A UNIX timestamp - */ - public function getTimestamp($resource) - { - $path = $this->getSourcePath($resource); - - if (!file_exists($path)) { - throw new \RuntimeException('There is no cached value for '.$resource); - } - - if (false === $mtime = @filemtime($path)) { - // @codeCoverageIgnoreStart - throw new \RuntimeException('Unable to determine file mtime for '.$path); - // @codeCoverageIgnoreEnd - } - - return $mtime; - } - - /** - * Returns the path where the file corresponding to the supplied cache key can be included from. - * - * @param string $resource A cache key - * - * @return string A file path - */ - private function getSourcePath($resource) - { - $key = md5($resource); - - return $this->dir.'/'.$key[0].'/'.$key.'.php'; - } -} diff --git a/src/Assetic/Cache/ExpiringCache.php b/src/Assetic/Cache/ExpiringCache.php deleted file mode 100644 index 6f972d806..000000000 --- a/src/Assetic/Cache/ExpiringCache.php +++ /dev/null @@ -1,58 +0,0 @@ - - */ -class ExpiringCache implements CacheInterface -{ - private $cache; - private $lifetime; - - public function __construct(CacheInterface $cache, $lifetime) - { - $this->cache = $cache; - $this->lifetime = $lifetime; - } - - public function has($key) - { - if ($this->cache->has($key)) { - if (time() < $this->cache->get($key.'.expires')) { - return true; - } - - $this->cache->remove($key.'.expires'); - $this->cache->remove($key); - } - - return false; - } - - public function get($key) - { - return $this->cache->get($key); - } - - public function set($key, $value) - { - $this->cache->set($key.'.expires', time() + $this->lifetime); - $this->cache->set($key, $value); - } - - public function remove($key) - { - $this->cache->remove($key.'.expires'); - $this->cache->remove($key); - } -} diff --git a/src/Assetic/Cache/FilesystemCache.php b/src/Assetic/Cache/FilesystemCache.php deleted file mode 100644 index aa5981757..000000000 --- a/src/Assetic/Cache/FilesystemCache.php +++ /dev/null @@ -1,61 +0,0 @@ -dir = $dir; - } - - public function has($key) - { - return file_exists($this->dir.'/'.$key); - } - - public function get($key) - { - $path = $this->dir.'/'.$key; - - if (!file_exists($path)) { - throw new RuntimeException('There is no cached value for '.$key); - } - - return file_get_contents($path); - } - - public function set($key, $value) - { - if (!is_dir($this->dir) && false === @mkdir($this->dir, 0777, true)) { - throw new RuntimeException('Unable to create directory '.$this->dir); - } - - $path = $this->dir.'/'.$key; - - if (false === @file_put_contents($path, $value)) { - throw new RuntimeException('Unable to write file '.$path); - } - - File::chmod($path); - } - - public function remove($key) - { - $path = $this->dir.'/'.$key; - - if (file_exists($path) && false === @unlink($path)) { - throw new RuntimeException('Unable to remove file '.$path); - } - } -} diff --git a/src/Assetic/Exception/Exception.php b/src/Assetic/Exception/Exception.php deleted file mode 100644 index b03c19976..000000000 --- a/src/Assetic/Exception/Exception.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ -interface Exception -{ -} diff --git a/src/Assetic/Exception/FilterException.php b/src/Assetic/Exception/FilterException.php deleted file mode 100644 index 4606cd128..000000000 --- a/src/Assetic/Exception/FilterException.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ -class FilterException extends \RuntimeException implements Exception -{ - private $originalMessage; - private $input; - - public function __construct($message, $code = 0, \Exception $previous = null) - { - parent::__construct($message, $code, $previous); - - $this->originalMessage = $message; - } - - public function setInput($input) - { - $this->input = $input; - $this->updateMessage(); - - return $this; - } - - public function getInput() - { - return $this->input; - } - - private function updateMessage() - { - $message = $this->originalMessage; - - if (!empty($this->input)) { - $message .= "\n\nInput:\n".$this->input; - } - - $this->message = $message; - } -} diff --git a/src/Assetic/Factory/AssetFactory.php b/src/Assetic/Factory/AssetFactory.php deleted file mode 100644 index add30143a..000000000 --- a/src/Assetic/Factory/AssetFactory.php +++ /dev/null @@ -1,422 +0,0 @@ - - */ -class AssetFactory -{ - private $root; - private $debug; - private $output; - private $workers; - private $am; - private $fm; - - /** - * Constructor. - * - * @param string $root The default root directory - * @param Boolean $debug Filters prefixed with a "?" will be omitted in debug mode - */ - public function __construct($root, $debug = false) - { - $this->root = rtrim($root, '/'); - $this->debug = $debug; - $this->output = 'assetic/*'; - $this->workers = array(); - } - - /** - * Sets debug mode for the current factory. - * - * @param Boolean $debug Debug mode - */ - public function setDebug($debug) - { - $this->debug = $debug; - } - - /** - * Checks if the factory is in debug mode. - * - * @return Boolean Debug mode - */ - public function isDebug() - { - return $this->debug; - } - - /** - * Sets the default output string. - * - * @param string $output The default output string - */ - public function setDefaultOutput($output) - { - $this->output = $output; - } - - /** - * Adds a factory worker. - * - * @param WorkerInterface $worker A worker - */ - public function addWorker(WorkerInterface $worker) - { - $this->workers[] = $worker; - } - - /** - * Returns the current asset manager. - * - * @return AssetManager|null The asset manager - */ - public function getAssetManager() - { - return $this->am; - } - - /** - * Sets the asset manager to use when creating asset references. - * - * @param AssetManager $am The asset manager - */ - public function setAssetManager(AssetManager $am) - { - $this->am = $am; - } - - /** - * Returns the current filter manager. - * - * @return FilterManager|null The filter manager - */ - public function getFilterManager() - { - return $this->fm; - } - - /** - * Sets the filter manager to use when adding filters. - * - * @param FilterManager $fm The filter manager - */ - public function setFilterManager(FilterManager $fm) - { - $this->fm = $fm; - } - - /** - * Creates a new asset. - * - * Prefixing a filter name with a question mark will cause it to be - * omitted when the factory is in debug mode. - * - * Available options: - * - * * output: An output string - * * name: An asset name for interpolation in output patterns - * * debug: Forces debug mode on or off for this asset - * * root: An array or string of more root directories - * - * @param array|string $inputs An array of input strings - * @param array|string $filters An array of filter names - * @param array $options An array of options - * - * @return AssetCollection An asset collection - */ - public function createAsset($inputs = array(), $filters = array(), array $options = array()) - { - if (!is_array($inputs)) { - $inputs = array($inputs); - } - - if (!is_array($filters)) { - $filters = array($filters); - } - - if (!isset($options['output'])) { - $options['output'] = $this->output; - } - - if (!isset($options['vars'])) { - $options['vars'] = array(); - } - - if (!isset($options['debug'])) { - $options['debug'] = $this->debug; - } - - if (!isset($options['root'])) { - $options['root'] = array($this->root); - } else { - if (!is_array($options['root'])) { - $options['root'] = array($options['root']); - } - - $options['root'][] = $this->root; - } - - if (!isset($options['name'])) { - $options['name'] = $this->generateAssetName($inputs, $filters, $options); - } - - $asset = $this->createAssetCollection(array(), $options); - $extensions = array(); - - // inner assets - foreach ($inputs as $input) { - if (is_array($input)) { - // nested formula - $asset->add(call_user_func_array(array($this, 'createAsset'), $input)); - } else { - $asset->add($this->parseInput($input, $options)); - $extensions[pathinfo($input, PATHINFO_EXTENSION)] = true; - } - } - - // filters - foreach ($filters as $filter) { - if ('?' != $filter[0]) { - $asset->ensureFilter($this->getFilter($filter)); - } elseif (!$options['debug']) { - $asset->ensureFilter($this->getFilter(substr($filter, 1))); - } - } - - // append variables - if (!empty($options['vars'])) { - $toAdd = array(); - foreach ($options['vars'] as $var) { - if (false !== strpos($options['output'], '{'.$var.'}')) { - continue; - } - - $toAdd[] = '{'.$var.'}'; - } - - if ($toAdd) { - $options['output'] = str_replace('*', '*.'.implode('.', $toAdd), $options['output']); - } - } - - // append consensus extension if missing - if (1 == count($extensions) && !pathinfo($options['output'], PATHINFO_EXTENSION) && $extension = key($extensions)) { - $options['output'] .= '.'.$extension; - } - - // output --> target url - $asset->setTargetPath(str_replace('*', $options['name'], $options['output'])); - - // apply workers and return - return $this->applyWorkers($asset); - } - - public function generateAssetName($inputs, $filters, $options = array()) - { - foreach (array_diff(array_keys($options), array('output', 'debug', 'root')) as $key) { - unset($options[$key]); - } - - ksort($options); - - return substr(sha1(serialize($inputs).serialize($filters).serialize($options)), 0, 7); - } - - public function getLastModified(AssetInterface $asset) - { - $mtime = 0; - foreach ($asset instanceof AssetCollectionInterface ? $asset : array($asset) as $leaf) { - $mtime = max($mtime, $leaf->getLastModified()); - - if (!$filters = $leaf->getFilters()) { - continue; - } - - $prevFilters = array(); - foreach ($filters as $filter) { - $prevFilters[] = $filter; - - if (!$filter instanceof DependencyExtractorInterface) { - continue; - } - - // extract children from leaf after running all preceeding filters - $clone = clone $leaf; - $clone->clearFilters(); - foreach (array_slice($prevFilters, 0, -1) as $prevFilter) { - $clone->ensureFilter($prevFilter); - } - $clone->load(); - - foreach ($filter->getChildren($this, $clone->getContent(), $clone->getSourceDirectory()) as $child) { - $mtime = max($mtime, $this->getLastModified($child)); - } - } - } - - return $mtime; - } - - /** - * Parses an input string string into an asset. - * - * The input string can be one of the following: - * - * * A reference: If the string starts with an "at" sign it will be interpreted as a reference to an asset in the asset manager - * * An absolute URL: If the string contains "://" or starts with "//" it will be interpreted as an HTTP asset - * * A glob: If the string contains a "*" it will be interpreted as a glob - * * A path: Otherwise the string is interpreted as a filesystem path - * - * Both globs and paths will be absolutized using the current root directory. - * - * @param string $input An input string - * @param array $options An array of options - * - * @return AssetInterface An asset - */ - protected function parseInput($input, array $options = array()) - { - if ('@' == $input[0]) { - return $this->createAssetReference(substr($input, 1)); - } - - if (false !== strpos($input, '://') || 0 === strpos($input, '//')) { - return $this->createHttpAsset($input, $options['vars']); - } - - if (self::isAbsolutePath($input)) { - if ($root = self::findRootDir($input, $options['root'])) { - $path = ltrim(substr($input, strlen($root)), '/'); - } else { - $path = null; - } - } else { - $root = $this->root; - $path = $input; - $input = $this->root.'/'.$path; - } - - if (false !== strpos($input, '*')) { - return $this->createGlobAsset($input, $root, $options['vars']); - } - - return $this->createFileAsset($input, $root, $path, $options['vars']); - } - - protected function createAssetCollection(array $assets = array(), array $options = array()) - { - return new AssetCollection($assets, array(), null, isset($options['vars']) ? $options['vars'] : array()); - } - - protected function createAssetReference($name) - { - if (!$this->am) { - throw new \LogicException('There is no asset manager.'); - } - - return new AssetReference($this->am, $name); - } - - protected function createHttpAsset($sourceUrl, $vars) - { - return new HttpAsset($sourceUrl, array(), false, $vars); - } - - protected function createGlobAsset($glob, $root = null, $vars = []) - { - return new GlobAsset($glob, array(), $root, $vars); - } - - protected function createFileAsset($source, $root = null, $path = null, $vars = []) - { - return new FileAsset($source, array(), $root, $path, $vars); - } - - protected function getFilter($name) - { - if (!$this->fm) { - throw new \LogicException('There is no filter manager.'); - } - - return $this->fm->get($name); - } - - /** - * Filters an asset collection through the factory workers. - * - * Each leaf asset will be processed first, followed by the asset - * collection itself. - * - * @param AssetCollectionInterface $asset An asset collection - * - * @return AssetCollectionInterface - */ - private function applyWorkers(AssetCollectionInterface $asset) - { - foreach ($asset as $leaf) { - foreach ($this->workers as $worker) { - $retval = $worker->process($leaf, $this); - - if ($retval instanceof AssetInterface && $leaf !== $retval) { - $asset->replaceLeaf($leaf, $retval); - } - } - } - - foreach ($this->workers as $worker) { - $retval = $worker->process($asset, $this); - - if ($retval instanceof AssetInterface) { - $asset = $retval; - } - } - - return $asset instanceof AssetCollectionInterface ? $asset : $this->createAssetCollection(array($asset)); - } - - private static function isAbsolutePath($path) - { - return '/' == $path[0] || '\\' == $path[0] || (3 < strlen($path) && ctype_alpha($path[0]) && $path[1] == ':' && ('\\' == $path[2] || '/' == $path[2])); - } - - /** - * Loops through the root directories and returns the first match. - * - * @param string $path An absolute path - * @param array $roots An array of root directories - * - * @return string|null The matching root directory, if found - */ - private static function findRootDir($path, array $roots) - { - foreach ($roots as $root) { - if (0 === strpos($path, $root)) { - return $root; - } - } - } -} diff --git a/src/Assetic/Factory/LazyAssetManager.php b/src/Assetic/Factory/LazyAssetManager.php deleted file mode 100644 index 65f08c10d..000000000 --- a/src/Assetic/Factory/LazyAssetManager.php +++ /dev/null @@ -1,208 +0,0 @@ - - */ -class LazyAssetManager extends AssetManager -{ - private $factory; - private $loaders; - private $resources; - private $formulae; - private $loaded; - private $loading; - - /** - * Constructor. - * - * @param AssetFactory $factory The asset factory - * @param array $loaders An array of loaders indexed by alias - */ - public function __construct(AssetFactory $factory, $loaders = array()) - { - $this->factory = $factory; - $this->loaders = array(); - $this->resources = array(); - $this->formulae = array(); - $this->loaded = false; - $this->loading = false; - - foreach ($loaders as $alias => $loader) { - $this->setLoader($alias, $loader); - } - } - - /** - * Adds a loader to the asset manager. - * - * @param string $alias An alias for the loader - * @param FormulaLoaderInterface $loader A loader - */ - public function setLoader($alias, FormulaLoaderInterface $loader) - { - $this->loaders[$alias] = $loader; - $this->loaded = false; - } - - /** - * Adds a resource to the asset manager. - * - * @param ResourceInterface $resource A resource - * @param string $loader The loader alias for this resource - */ - public function addResource(ResourceInterface $resource, $loader) - { - $this->resources[$loader][] = $resource; - $this->loaded = false; - } - - /** - * Returns an array of resources. - * - * @return array An array of resources - */ - public function getResources() - { - $resources = array(); - foreach ($this->resources as $r) { - $resources = array_merge($resources, $r); - } - - return $resources; - } - - /** - * Checks for an asset formula. - * - * @param string $name An asset name - * - * @return Boolean If there is a formula - */ - public function hasFormula($name) - { - if (!$this->loaded) { - $this->load(); - } - - return isset($this->formulae[$name]); - } - - /** - * Returns an asset's formula. - * - * @param string $name An asset name - * - * @return array The formula - * - * @throws \InvalidArgumentException If there is no formula by that name - */ - public function getFormula($name) - { - if (!$this->loaded) { - $this->load(); - } - - if (!isset($this->formulae[$name])) { - throw new \InvalidArgumentException(sprintf('There is no "%s" formula.', $name)); - } - - return $this->formulae[$name]; - } - - /** - * Sets a formula on the asset manager. - * - * @param string $name An asset name - * @param array $formula A formula - */ - public function setFormula($name, array $formula) - { - $this->formulae[$name] = $formula; - } - - /** - * Loads formulae from resources. - * - * @throws \LogicException If a resource has been added to an invalid loader - */ - public function load() - { - if ($this->loading) { - return; - } - - if ($diff = array_diff(array_keys($this->resources), array_keys($this->loaders))) { - throw new \LogicException('The following loader(s) are not registered: '.implode(', ', $diff)); - } - - $this->loading = true; - - foreach ($this->resources as $loader => $resources) { - foreach ($resources as $resource) { - $this->formulae = array_replace($this->formulae, $this->loaders[$loader]->load($resource)); - } - } - - $this->loaded = true; - $this->loading = false; - } - - public function get($name) - { - if (!$this->loaded) { - $this->load(); - } - - if (!parent::has($name) && isset($this->formulae[$name])) { - list($inputs, $filters, $options) = $this->formulae[$name]; - $options['name'] = $name; - parent::set($name, $this->factory->createAsset($inputs, $filters, $options)); - } - - return parent::get($name); - } - - public function has($name) - { - if (!$this->loaded) { - $this->load(); - } - - return isset($this->formulae[$name]) || parent::has($name); - } - - public function getNames() - { - if (!$this->loaded) { - $this->load(); - } - - return array_unique(array_merge(parent::getNames(), array_keys($this->formulae))); - } - - public function isDebug() - { - return $this->factory->isDebug(); - } - - public function getLastModified(AssetInterface $asset) - { - return $this->factory->getLastModified($asset); - } -} diff --git a/src/Assetic/Factory/Loader/BasePhpFormulaLoader.php b/src/Assetic/Factory/Loader/BasePhpFormulaLoader.php deleted file mode 100644 index 78522debc..000000000 --- a/src/Assetic/Factory/Loader/BasePhpFormulaLoader.php +++ /dev/null @@ -1,162 +0,0 @@ - - */ -abstract class BasePhpFormulaLoader implements FormulaLoaderInterface -{ - protected $factory; - protected $prototypes; - - public function __construct(AssetFactory $factory) - { - $this->factory = $factory; - $this->prototypes = array(); - - foreach ($this->registerPrototypes() as $prototype => $options) { - $this->addPrototype($prototype, $options); - } - } - - public function addPrototype($prototype, array $options = array()) - { - $tokens = token_get_all('prototypes[$prototype] = array($tokens, $options); - } - - public function load(ResourceInterface $resource) - { - if (!$nbProtos = count($this->prototypes)) { - throw new \LogicException('There are no prototypes registered.'); - } - - $buffers = array_fill(0, $nbProtos, ''); - $bufferLevels = array_fill(0, $nbProtos, 0); - $buffersInWildcard = array(); - - $tokens = token_get_all($resource->getContent()); - $calls = array(); - - while ($token = array_shift($tokens)) { - $current = self::tokenToString($token); - // loop through each prototype (by reference) - foreach (array_keys($this->prototypes) as $i) { - $prototype = & $this->prototypes[$i][0]; - $options = $this->prototypes[$i][1]; - $buffer = & $buffers[$i]; - $level = & $bufferLevels[$i]; - - if (isset($buffersInWildcard[$i])) { - switch ($current) { - case '(': - ++$level; - break; - case ')': - --$level; - break; - } - - $buffer .= $current; - - if (!$level) { - $calls[] = array($buffer.';', $options); - $buffer = ''; - unset($buffersInWildcard[$i]); - } - } elseif ($current == self::tokenToString(current($prototype))) { - $buffer .= $current; - if ('*' == self::tokenToString(next($prototype))) { - $buffersInWildcard[$i] = true; - ++$level; - } - } else { - reset($prototype); - unset($buffersInWildcard[$i]); - $buffer = ''; - } - } - } - - $formulae = array(); - foreach ($calls as $call) { - $formulae += call_user_func_array(array($this, 'processCall'), $call); - } - - return $formulae; - } - - private function processCall($call, array $protoOptions = array()) - { - $tmp = FilesystemUtils::createTemporaryFile('php_formula_loader'); - file_put_contents($tmp, implode("\n", array( - 'registerSetupCode(), - $call, - 'echo serialize($_call);', - ))); - $args = unserialize(shell_exec('php '.escapeshellarg($tmp))); - unlink($tmp); - - $inputs = isset($args[0]) ? self::argumentToArray($args[0]) : array(); - $filters = isset($args[1]) ? self::argumentToArray($args[1]) : array(); - $options = isset($args[2]) ? $args[2] : array(); - - if (!isset($options['debug'])) { - $options['debug'] = $this->factory->isDebug(); - } - - if (!is_array($options)) { - throw new \RuntimeException('The third argument must be omitted, null or an array.'); - } - - // apply the prototype options - $options += $protoOptions; - - if (!isset($options['name'])) { - $options['name'] = $this->factory->generateAssetName($inputs, $filters, $options); - } - - return array($options['name'] => array($inputs, $filters, $options)); - } - - /** - * Returns an array of prototypical calls and options. - * - * @return array Prototypes and options - */ - abstract protected function registerPrototypes(); - - /** - * Returns setup code for the reflection scriptlet. - * - * @return string Some PHP setup code - */ - abstract protected function registerSetupCode(); - - protected static function tokenToString($token) - { - return is_array($token) ? $token[1] : $token; - } - - protected static function argumentToArray($argument) - { - return is_array($argument) ? $argument : array_filter(array_map('trim', explode(',', $argument))); - } -} diff --git a/src/Assetic/Factory/Loader/CachedFormulaLoader.php b/src/Assetic/Factory/Loader/CachedFormulaLoader.php deleted file mode 100644 index 7ac943979..000000000 --- a/src/Assetic/Factory/Loader/CachedFormulaLoader.php +++ /dev/null @@ -1,66 +0,0 @@ - - */ -class CachedFormulaLoader implements FormulaLoaderInterface -{ - private $loader; - private $configCache; - private $debug; - - /** - * Constructor. - * - * When the loader is in debug mode it will ensure the cached formulae - * are fresh before returning them. - * - * @param FormulaLoaderInterface $loader A formula loader - * @param ConfigCache $configCache A config cache - * @param Boolean $debug The debug mode - */ - public function __construct(FormulaLoaderInterface $loader, ConfigCache $configCache, $debug = false) - { - $this->loader = $loader; - $this->configCache = $configCache; - $this->debug = $debug; - } - - public function load(ResourceInterface $resources) - { - if (!$resources instanceof IteratorResourceInterface) { - $resources = array($resources); - } - - $formulae = array(); - - foreach ($resources as $resource) { - $id = (string) $resource; - if (!$this->configCache->has($id) || ($this->debug && !$resource->isFresh($this->configCache->getTimestamp($id)))) { - $formulae += $this->loader->load($resource); - $this->configCache->set($id, $formulae); - } else { - $formulae += $this->configCache->get($id); - } - } - - return $formulae; - } -} diff --git a/src/Assetic/Factory/Loader/FormulaLoaderInterface.php b/src/Assetic/Factory/Loader/FormulaLoaderInterface.php deleted file mode 100644 index e902ad2e1..000000000 --- a/src/Assetic/Factory/Loader/FormulaLoaderInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -interface FormulaLoaderInterface -{ - /** - * Loads formulae from a resource. - * - * Formulae should be loaded the same regardless of the current debug - * mode. Debug considerations should happen downstream. - * - * @param ResourceInterface $resource A resource - * - * @return array An array of formulae - */ - public function load(ResourceInterface $resource); -} diff --git a/src/Assetic/Factory/Loader/FunctionCallsFormulaLoader.php b/src/Assetic/Factory/Loader/FunctionCallsFormulaLoader.php deleted file mode 100644 index ceb46707b..000000000 --- a/src/Assetic/Factory/Loader/FunctionCallsFormulaLoader.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -class FunctionCallsFormulaLoader extends BasePhpFormulaLoader -{ - protected function registerPrototypes() - { - return array( - 'assetic_javascripts(*)' => array('output' => 'js/*.js'), - 'assetic_stylesheets(*)' => array('output' => 'css/*.css'), - 'assetic_image(*)' => array('output' => 'images/*'), - ); - } - - protected function registerSetupCode() - { - return <<<'EOF' -function assetic_javascripts() -{ - global $_call; - $_call = func_get_args(); -} - -function assetic_stylesheets() -{ - global $_call; - $_call = func_get_args(); -} - -function assetic_image() -{ - global $_call; - $_call = func_get_args(); -} - -EOF; - } -} diff --git a/src/Assetic/Factory/Resource/CoalescingDirectoryResource.php b/src/Assetic/Factory/Resource/CoalescingDirectoryResource.php deleted file mode 100644 index c9588c5d8..000000000 --- a/src/Assetic/Factory/Resource/CoalescingDirectoryResource.php +++ /dev/null @@ -1,110 +0,0 @@ - - */ -class CoalescingDirectoryResource implements IteratorResourceInterface -{ - private $directories; - - public function __construct($directories) - { - $this->directories = array(); - - foreach ($directories as $directory) { - $this->addDirectory($directory); - } - } - - public function addDirectory(IteratorResourceInterface $directory) - { - $this->directories[] = $directory; - } - - public function isFresh($timestamp) - { - foreach ($this->getFileResources() as $file) { - if (!$file->isFresh($timestamp)) { - return false; - } - } - - return true; - } - - public function getContent() - { - $parts = array(); - foreach ($this->getFileResources() as $file) { - $parts[] = $file->getContent(); - } - - return implode("\n", $parts); - } - - /** - * Returns a string to uniquely identify the current resource. - * - * @return string An identifying string - */ - public function __toString() - { - $parts = array(); - foreach ($this->directories as $directory) { - $parts[] = (string) $directory; - } - - return implode(',', $parts); - } - - public function getIterator() - { - return new \ArrayIterator($this->getFileResources()); - } - - /** - * Returns the relative version of a filename. - * - * @param ResourceInterface $file The file - * @param ResourceInterface $directory The directory - * - * @return string The name to compare with files from other directories - */ - protected function getRelativeName(ResourceInterface $file, ResourceInterface $directory) - { - return substr((string) $file, strlen((string) $directory)); - } - - /** - * Performs the coalesce. - * - * @return array An array of file resources - */ - private function getFileResources() - { - $paths = array(); - - foreach ($this->directories as $directory) { - foreach ($directory as $file) { - $relative = $this->getRelativeName($file, $directory); - - if (!isset($paths[$relative])) { - $paths[$relative] = $file; - } - } - } - - return array_values($paths); - } -} diff --git a/src/Assetic/Factory/Resource/DirectoryResource.php b/src/Assetic/Factory/Resource/DirectoryResource.php deleted file mode 100644 index a83c7838e..000000000 --- a/src/Assetic/Factory/Resource/DirectoryResource.php +++ /dev/null @@ -1,82 +0,0 @@ - - */ -class DirectoryResource implements IteratorResourceInterface -{ - private $path; - private $pattern; - - /** - * Constructor. - * - * @param string $path A directory path - * @param string $pattern A filename pattern - */ - public function __construct($path, $pattern = null) - { - if (DIRECTORY_SEPARATOR != substr($path, -1)) { - $path .= DIRECTORY_SEPARATOR; - } - - $this->path = $path; - $this->pattern = $pattern; - } - - public function isFresh($timestamp) - { - if (!is_dir($this->path) || filemtime($this->path) > $timestamp) { - return false; - } - - foreach ($this as $resource) { - if (!$resource->isFresh($timestamp)) { - return false; - } - } - - return true; - } - - /** - * Returns the combined content of all inner resources. - */ - public function getContent() - { - $content = array(); - foreach ($this as $resource) { - $content[] = $resource->getContent(); - } - - return implode("\n", $content); - } - - public function __toString() - { - return $this->path; - } - - public function getIterator() - { - return is_dir($this->path) - ? new DirectoryResourceIterator($this->getInnerIterator()) - : new \EmptyIterator(); - } - - protected function getInnerIterator() - { - return new DirectoryResourceFilterIterator(new \RecursiveDirectoryIterator($this->path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS), $this->pattern); - } -} diff --git a/src/Assetic/Factory/Resource/DirectoryResourceFilterIterator.php b/src/Assetic/Factory/Resource/DirectoryResourceFilterIterator.php deleted file mode 100644 index 22fcb4dfc..000000000 --- a/src/Assetic/Factory/Resource/DirectoryResourceFilterIterator.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @access private - */ -class DirectoryResourceFilterIterator extends \RecursiveFilterIterator -{ - protected $pattern; - - public function __construct(\RecursiveDirectoryIterator $iterator, $pattern = null) - { - parent::__construct($iterator); - - $this->pattern = $pattern; - } - - public function accept() - { - $file = $this->current(); - $name = $file->getBasename(); - - if ($file->isDir()) { - return '.' != $name[0]; - } - - return null === $this->pattern || 0 < preg_match($this->pattern, $name); - } - - public function getChildren() - { - return new self(new \RecursiveDirectoryIterator($this->current()->getPathname(), \RecursiveDirectoryIterator::FOLLOW_SYMLINKS), $this->pattern); - } -} diff --git a/src/Assetic/Factory/Resource/DirectoryResourceIterator.php b/src/Assetic/Factory/Resource/DirectoryResourceIterator.php deleted file mode 100644 index 3e8f75c87..000000000 --- a/src/Assetic/Factory/Resource/DirectoryResourceIterator.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @access private - */ -class DirectoryResourceIterator extends \RecursiveIteratorIterator -{ - public function current() - { - return new FileResource(parent::current()->getPathname()); - } -} diff --git a/src/Assetic/Factory/Resource/FileResource.php b/src/Assetic/Factory/Resource/FileResource.php deleted file mode 100644 index de49c259e..000000000 --- a/src/Assetic/Factory/Resource/FileResource.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ -class FileResource implements ResourceInterface -{ - private $path; - - /** - * Constructor. - * - * @param string $path The path to a file - */ - public function __construct($path) - { - $this->path = $path; - } - - public function isFresh($timestamp) - { - return file_exists($this->path) && filemtime($this->path) <= $timestamp; - } - - public function getContent() - { - return file_exists($this->path) ? file_get_contents($this->path) : ''; - } - - public function __toString() - { - return $this->path; - } -} diff --git a/src/Assetic/Factory/Resource/IteratorResourceInterface.php b/src/Assetic/Factory/Resource/IteratorResourceInterface.php deleted file mode 100644 index ef9397549..000000000 --- a/src/Assetic/Factory/Resource/IteratorResourceInterface.php +++ /dev/null @@ -1,19 +0,0 @@ - - */ -interface IteratorResourceInterface extends ResourceInterface, \IteratorAggregate -{ -} diff --git a/src/Assetic/Factory/Resource/ResourceInterface.php b/src/Assetic/Factory/Resource/ResourceInterface.php deleted file mode 100644 index 23ee786ce..000000000 --- a/src/Assetic/Factory/Resource/ResourceInterface.php +++ /dev/null @@ -1,41 +0,0 @@ - - */ -interface ResourceInterface -{ - /** - * Checks if a timestamp represents the latest resource. - * - * @param integer $timestamp A UNIX timestamp - * - * @return Boolean True if the timestamp is up to date - */ - public function isFresh($timestamp); - - /** - * Returns the content of the resource. - * - * @return string The content - */ - public function getContent(); - - /** - * Returns a unique string for the current resource. - * - * @return string A unique string to identity the current resource - */ - public function __toString(); -} diff --git a/src/Assetic/Factory/Worker/CacheBustingWorker.php b/src/Assetic/Factory/Worker/CacheBustingWorker.php deleted file mode 100644 index 57baf6a92..000000000 --- a/src/Assetic/Factory/Worker/CacheBustingWorker.php +++ /dev/null @@ -1,68 +0,0 @@ - - */ -class CacheBustingWorker implements WorkerInterface -{ - private $separator; - - public function __construct($separator = '-') - { - $this->separator = $separator; - } - - public function process(AssetInterface $asset, AssetFactory $factory) - { - if (!$path = $asset->getTargetPath()) { - // no path to work with - return; - } - - if (!$search = pathinfo($path, PATHINFO_EXTENSION)) { - // nothing to replace - return; - } - - $replace = $this->separator.$this->getHash($asset, $factory).'.'.$search; - if (preg_match('/'.preg_quote($replace, '/').'$/', $path)) { - // already replaced - return; - } - - $asset->setTargetPath( - preg_replace('/\.'.preg_quote($search, '/').'$/', $replace, $path) - ); - } - - protected function getHash(AssetInterface $asset, AssetFactory $factory) - { - $hash = hash_init('sha1'); - - hash_update($hash, $factory->getLastModified($asset)); - - if ($asset instanceof AssetCollectionInterface) { - foreach ($asset as $i => $leaf) { - $sourcePath = $leaf->getSourcePath(); - hash_update($hash, $sourcePath ?: $i); - } - } - - return substr(hash_final($hash), 0, 7); - } -} diff --git a/src/Assetic/Factory/Worker/EnsureFilterWorker.php b/src/Assetic/Factory/Worker/EnsureFilterWorker.php deleted file mode 100644 index efc94956a..000000000 --- a/src/Assetic/Factory/Worker/EnsureFilterWorker.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @todo A better asset-matcher mechanism - */ -class EnsureFilterWorker implements WorkerInterface -{ - const CHECK_SOURCE = 1; - const CHECK_TARGET = 2; - - private $pattern; - private $filter; - private $flags; - - /** - * Constructor. - * - * @param string $pattern A regex for checking the asset's target URL - * @param FilterInterface $filter A filter to apply if the regex matches - * @param integer $flags Flags for what to check - */ - public function __construct($pattern, FilterInterface $filter, $flags = null) - { - if (null === $flags) { - $flags = self::CHECK_SOURCE | self::CHECK_TARGET; - } - - $this->pattern = $pattern; - $this->filter = $filter; - $this->flags = $flags; - } - - public function process(AssetInterface $asset, AssetFactory $factory) - { - if ( - (self::CHECK_SOURCE === (self::CHECK_SOURCE & $this->flags) && preg_match($this->pattern, $asset->getSourcePath())) - || - (self::CHECK_TARGET === (self::CHECK_TARGET & $this->flags) && preg_match($this->pattern, $asset->getTargetPath())) - ) { - $asset->ensureFilter($this->filter); - } - } -} diff --git a/src/Assetic/Factory/Worker/WorkerInterface.php b/src/Assetic/Factory/Worker/WorkerInterface.php deleted file mode 100644 index 3216fd8c9..000000000 --- a/src/Assetic/Factory/Worker/WorkerInterface.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ -interface WorkerInterface -{ - /** - * Processes an asset. - * - * @param AssetInterface $asset An asset - * @param AssetFactory $factory The factory - * - * @return AssetInterface|null May optionally return a replacement asset - */ - public function process(AssetInterface $asset, AssetFactory $factory); -} diff --git a/src/Assetic/Filter/BaseCssFilter.php b/src/Assetic/Filter/BaseCssFilter.php deleted file mode 100644 index 966efe1f0..000000000 --- a/src/Assetic/Filter/BaseCssFilter.php +++ /dev/null @@ -1,52 +0,0 @@ - - */ -abstract class BaseCssFilter implements FilterInterface -{ - /** - * @see CssUtils::filterReferences() - */ - protected function filterReferences($content, $callback, $limit = -1, &$count = 0) - { - return CssUtils::filterReferences($content, $callback, $limit, $count); - } - - /** - * @see CssUtils::filterUrls() - */ - protected function filterUrls($content, $callback, $limit = -1, &$count = 0) - { - return CssUtils::filterUrls($content, $callback, $limit, $count); - } - - /** - * @see CssUtils::filterImports() - */ - protected function filterImports($content, $callback, $limit = -1, &$count = 0, $includeUrl = true) - { - return CssUtils::filterImports($content, $callback, $limit, $count, $includeUrl); - } - - /** - * @see CssUtils::filterIEFilters() - */ - protected function filterIEFilters($content, $callback, $limit = -1, &$count = 0) - { - return CssUtils::filterIEFilters($content, $callback, $limit, $count); - } -} diff --git a/src/Assetic/Filter/CallablesFilter.php b/src/Assetic/Filter/CallablesFilter.php deleted file mode 100644 index 15f425183..000000000 --- a/src/Assetic/Filter/CallablesFilter.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ -class CallablesFilter implements FilterInterface, DependencyExtractorInterface -{ - private $loader; - private $dumper; - private $extractor; - - /** - * @param callable|null $loader - * @param callable|null $dumper - * @param callable|null $extractor - */ - public function __construct($loader = null, $dumper = null, $extractor = null) - { - $this->loader = $loader; - $this->dumper = $dumper; - $this->extractor = $extractor; - } - - public function filterLoad(AssetInterface $asset) - { - if (null !== $callable = $this->loader) { - $callable($asset); - } - } - - public function filterDump(AssetInterface $asset) - { - if (null !== $callable = $this->dumper) { - $callable($asset); - } - } - - public function getChildren(AssetFactory $factory, $content, $loadPath = null) - { - if (null !== $callable = $this->extractor) { - return $callable($factory, $content, $loadPath); - } - - return array(); - } -} diff --git a/src/Assetic/Filter/CssCacheBustingFilter.php b/src/Assetic/Filter/CssCacheBustingFilter.php deleted file mode 100644 index 694103155..000000000 --- a/src/Assetic/Filter/CssCacheBustingFilter.php +++ /dev/null @@ -1,63 +0,0 @@ - - */ -class CssCacheBustingFilter extends BaseCssFilter -{ - private $version; - private $format = '%s?%s'; - - public function setVersion($version) - { - $this->version = $version; - } - - public function setFormat($versionFormat) - { - $this->format = $versionFormat; - } - - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - if (!$this->version) { - return; - } - - $version = $this->version; - $format = $this->format; - - $asset->setContent($this->filterReferences( - $asset->getContent(), - function ($matches) use ($version, $format) { - if (0 === strpos($matches['url'], 'data:')) { - return $matches[0]; - } - - return str_replace( - $matches['url'], - sprintf($format, $matches['url'], $version), - $matches[0] - ); - } - )); - } -} diff --git a/src/Assetic/Filter/CssImportFilter.php b/src/Assetic/Filter/CssImportFilter.php deleted file mode 100644 index b12f3c2c8..000000000 --- a/src/Assetic/Filter/CssImportFilter.php +++ /dev/null @@ -1,106 +0,0 @@ - - */ -class CssImportFilter extends BaseCssFilter implements DependencyExtractorInterface -{ - private $importFilter; - - /** - * Constructor. - * - * @param FilterInterface $importFilter Filter for each imported asset - */ - public function __construct(FilterInterface $importFilter = null) - { - $this->importFilter = $importFilter ?: new CssRewriteFilter(); - } - - public function filterLoad(AssetInterface $asset) - { - $importFilter = $this->importFilter; - $sourceRoot = $asset->getSourceRoot(); - $sourcePath = $asset->getSourcePath(); - - $callback = function ($matches) use ($importFilter, $sourceRoot, $sourcePath) { - if (!$matches['url'] || null === $sourceRoot) { - return $matches[0]; - } - - $importRoot = $sourceRoot; - - if (false !== strpos($matches['url'], '://')) { - // absolute - list($importScheme, $tmp) = explode('://', $matches['url'], 2); - list($importHost, $importPath) = explode('/', $tmp, 2); - $importRoot = $importScheme.'://'.$importHost; - } elseif (0 === strpos($matches['url'], '//')) { - // protocol-relative - list($importHost, $importPath) = explode('/', substr($matches['url'], 2), 2); - $importRoot = '//'.$importHost; - } elseif ('/' == $matches['url'][0]) { - // root-relative - $importPath = substr($matches['url'], 1); - } elseif (null !== $sourcePath) { - // document-relative - $importPath = $matches['url']; - if ('.' != $sourceDir = dirname($sourcePath)) { - $importPath = $sourceDir.'/'.$importPath; - } - } else { - return $matches[0]; - } - - $importSource = $importRoot.'/'.$importPath; - if (false !== strpos($importSource, '://') || 0 === strpos($importSource, '//')) { - $import = new HttpAsset($importSource, array($importFilter), true); - } elseif ('css' != pathinfo($importPath, PATHINFO_EXTENSION) || !file_exists($importSource)) { - // ignore non-css and non-existant imports - return $matches[0]; - } else { - $import = new FileAsset($importSource, array($importFilter), $importRoot, $importPath); - } - - $import->setTargetPath($sourcePath); - - return $import->dump(); - }; - - $content = $asset->getContent(); - $lastHash = md5($content); - - do { - $content = $this->filterImports($content, $callback); - $hash = md5($content); - } while ($lastHash != $hash && $lastHash = $hash); - - $asset->setContent($content); - } - - public function filterDump(AssetInterface $asset) - { - } - - public function getChildren(AssetFactory $factory, $content, $loadPath = null) - { - // todo - return array(); - } -} diff --git a/src/Assetic/Filter/CssMinFilter.php b/src/Assetic/Filter/CssMinFilter.php deleted file mode 100644 index 5538ad869..000000000 --- a/src/Assetic/Filter/CssMinFilter.php +++ /dev/null @@ -1,70 +0,0 @@ - - */ -class CssMinFilter implements FilterInterface -{ - private $filters; - private $plugins; - - public function __construct() - { - $this->filters = array(); - $this->plugins = array(); - } - - public function setFilters(array $filters) - { - $this->filters = $filters; - } - - public function setFilter($name, $value) - { - $this->filters[$name] = $value; - } - - public function setPlugins(array $plugins) - { - $this->plugins = $plugins; - } - - public function setPlugin($name, $value) - { - $this->plugins[$name] = $value; - } - - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - $filters = $this->filters; - $plugins = $this->plugins; - - if (isset($filters['ImportImports']) && true === $filters['ImportImports']) { - if ($dir = $asset->getSourceDirectory()) { - $filters['ImportImports'] = array('BasePath' => $dir); - } else { - unset($filters['ImportImports']); - } - } - - $asset->setContent(\CssMin::minify($asset->getContent(), $filters, $plugins)); - } -} diff --git a/src/Assetic/Filter/CssRewriteFilter.php b/src/Assetic/Filter/CssRewriteFilter.php deleted file mode 100644 index cc21f369b..000000000 --- a/src/Assetic/Filter/CssRewriteFilter.php +++ /dev/null @@ -1,100 +0,0 @@ - - */ -class CssRewriteFilter extends BaseCssFilter -{ - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - $sourceBase = $asset->getSourceRoot(); - $sourcePath = $asset->getSourcePath(); - $targetPath = $asset->getTargetPath(); - - if (null === $sourcePath || null === $targetPath || $sourcePath == $targetPath) { - return; - } - - // learn how to get from the target back to the source - if (false !== strpos($sourceBase, '://')) { - list($scheme, $url) = explode('://', $sourceBase.'/'.$sourcePath, 2); - list($host, $path) = explode('/', $url, 2); - - $host = $scheme.'://'.$host.'/'; - $path = false === strpos($path, '/') ? '' : dirname($path); - $path .= '/'; - } else { - // assume source and target are on the same host - $host = ''; - - // pop entries off the target until it fits in the source - if ('.' == dirname($sourcePath)) { - $path = str_repeat('../', substr_count($targetPath, '/')); - } elseif ('.' == $targetDir = dirname($targetPath)) { - $path = dirname($sourcePath).'/'; - } else { - $path = ''; - while (0 !== strpos($sourcePath, $targetDir)) { - if (false !== $pos = strrpos($targetDir, '/')) { - $targetDir = substr($targetDir, 0, $pos); - $path .= '../'; - } else { - $targetDir = ''; - $path .= '../'; - break; - } - } - $path .= ltrim(substr(dirname($sourcePath).'/', strlen($targetDir)), '/'); - } - } - - $content = $this->filterReferences($asset->getContent(), function ($matches) use ($host, $path) { - if (false !== strpos($matches['url'], '://') || 0 === strpos($matches['url'], '//') || 0 === strpos($matches['url'], 'data:')) { - // absolute or protocol-relative or data uri - return $matches[0]; - } - - if (isset($matches['url'][0]) && '/' == $matches['url'][0]) { - // root relative - return str_replace($matches['url'], $host.$matches['url'], $matches[0]); - } - - // document relative - $url = $matches['url']; - while (0 === strpos($url, '../') && 2 <= substr_count($path, '/')) { - $path = substr($path, 0, strrpos(rtrim($path, '/'), '/') + 1); - $url = substr($url, 3); - } - - $parts = array(); - foreach (explode('/', $host.$path.$url) as $part) { - if ('..' === $part && count($parts) && '..' !== end($parts)) { - array_pop($parts); - } else { - $parts[] = $part; - } - } - - return str_replace($matches['url'], implode('/', $parts), $matches[0]); - }); - - $asset->setContent($content); - } -} diff --git a/src/Assetic/Filter/DependencyExtractorInterface.php b/src/Assetic/Filter/DependencyExtractorInterface.php deleted file mode 100644 index 047b0006c..000000000 --- a/src/Assetic/Filter/DependencyExtractorInterface.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -interface DependencyExtractorInterface extends FilterInterface -{ - /** - * Returns child assets. - * - * @param AssetFactory $factory The asset factory - * @param string $content The asset content - * @param string $loadPath An optional load path - * - * @return AssetInterface[] Child assets - */ - public function getChildren(AssetFactory $factory, $content, $loadPath = null); -} diff --git a/src/Assetic/Filter/FilterCollection.php b/src/Assetic/Filter/FilterCollection.php deleted file mode 100644 index 2543793e4..000000000 --- a/src/Assetic/Filter/FilterCollection.php +++ /dev/null @@ -1,80 +0,0 @@ - - */ -class FilterCollection implements FilterInterface, \IteratorAggregate, \Countable -{ - private $filters = array(); - - public function __construct($filters = array()) - { - foreach ($filters as $filter) { - $this->ensure($filter); - } - } - - /** - * Checks that the current collection contains the supplied filter. - * - * If the supplied filter is another filter collection, each of its - * filters will be checked. - */ - public function ensure(FilterInterface $filter) - { - if ($filter instanceof \Traversable) { - foreach ($filter as $f) { - $this->ensure($f); - } - } elseif (!in_array($filter, $this->filters, true)) { - $this->filters[] = $filter; - } - } - - public function all() - { - return $this->filters; - } - - public function clear() - { - $this->filters = array(); - } - - public function filterLoad(AssetInterface $asset) - { - foreach ($this->filters as $filter) { - $filter->filterLoad($asset); - } - } - - public function filterDump(AssetInterface $asset) - { - foreach ($this->filters as $filter) { - $filter->filterDump($asset); - } - } - - public function getIterator() - { - return new \ArrayIterator($this->filters); - } - - public function count() - { - return count($this->filters); - } -} diff --git a/src/Assetic/Filter/FilterInterface.php b/src/Assetic/Filter/FilterInterface.php deleted file mode 100644 index 1714a8ad8..000000000 --- a/src/Assetic/Filter/FilterInterface.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ -interface FilterInterface -{ - /** - * Filters an asset after it has been loaded. - * - * @param AssetInterface $asset An asset - */ - public function filterLoad(AssetInterface $asset); - - /** - * Filters an asset just before it's dumped. - * - * @param AssetInterface $asset An asset - */ - public function filterDump(AssetInterface $asset); -} diff --git a/src/Assetic/Filter/HashableInterface.php b/src/Assetic/Filter/HashableInterface.php deleted file mode 100644 index 88149450a..000000000 --- a/src/Assetic/Filter/HashableInterface.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ -interface HashableInterface -{ - /** - * Generates a hash for the object - * - * @return string Object hash - */ - public function hash(); -} diff --git a/src/Assetic/Filter/JSMinFilter.php b/src/Assetic/Filter/JSMinFilter.php deleted file mode 100644 index b2b245625..000000000 --- a/src/Assetic/Filter/JSMinFilter.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class JSMinFilter implements FilterInterface -{ - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - $asset->setContent(\JSMin::minify($asset->getContent())); - } -} diff --git a/src/Assetic/Filter/JSMinPlusFilter.php b/src/Assetic/Filter/JSMinPlusFilter.php deleted file mode 100644 index 2fcf92e9b..000000000 --- a/src/Assetic/Filter/JSMinPlusFilter.php +++ /dev/null @@ -1,32 +0,0 @@ - - */ -class JSMinPlusFilter implements FilterInterface -{ - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - $asset->setContent(\JSMinPlus::minify($asset->getContent())); - } -} diff --git a/src/Assetic/Filter/JSqueezeFilter.php b/src/Assetic/Filter/JSqueezeFilter.php deleted file mode 100644 index 5a2127ac3..000000000 --- a/src/Assetic/Filter/JSqueezeFilter.php +++ /dev/null @@ -1,75 +0,0 @@ - - */ -class JSqueezeFilter implements FilterInterface -{ - private $singleLine = true; - private $keepImportantComments = true; - private $className; - private $specialVarRx = false; - private $defaultRx; - - public function __construct() - { - // JSqueeze is namespaced since 2.x, this works with both 1.x and 2.x - if (class_exists('\\Patchwork\\JSqueeze')) { - $this->className = '\\Patchwork\\JSqueeze'; - $this->defaultRx = \Patchwork\JSqueeze::SPECIAL_VAR_PACKER; - } else { - $this->className = '\\JSqueeze'; - $this->defaultRx = \JSqueeze::SPECIAL_VAR_RX; - } - } - - public function setSingleLine($bool) - { - $this->singleLine = (bool) $bool; - } - - // call setSpecialVarRx(true) to enable global var/method/property - // renaming with the default regex (for 1.x or 2.x) - public function setSpecialVarRx($specialVarRx) - { - if (true === $specialVarRx) { - $this->specialVarRx = $this->defaultRx; - } else { - $this->specialVarRx = $specialVarRx; - } - } - - public function keepImportantComments($bool) - { - $this->keepImportantComments = (bool) $bool; - } - - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - $parser = new $this->className(); - $asset->setContent($parser->squeeze( - $asset->getContent(), - $this->singleLine, - $this->keepImportantComments, - $this->specialVarRx - )); - } -} diff --git a/src/Assetic/Filter/LessphpFilter.php b/src/Assetic/Filter/LessphpFilter.php deleted file mode 100644 index 2478517b0..000000000 --- a/src/Assetic/Filter/LessphpFilter.php +++ /dev/null @@ -1,165 +0,0 @@ - - * @author Kris Wallsmith - */ -class LessphpFilter implements DependencyExtractorInterface -{ - private $presets = array(); - private $formatter; - private $preserveComments; - private $customFunctions = array(); - private $options = array(); - - /** - * Lessphp Load Paths - * - * @var array - */ - protected $loadPaths = array(); - - /** - * Adds a load path to the paths used by lessphp - * - * @param string $path Load Path - */ - public function addLoadPath($path) - { - $this->loadPaths[] = $path; - } - - /** - * Sets load paths used by lessphp - * - * @param array $loadPaths Load paths - */ - public function setLoadPaths(array $loadPaths) - { - $this->loadPaths = $loadPaths; - } - - public function setPresets(array $presets) - { - $this->presets = $presets; - } - - public function setOptions(array $options) - { - $this->options = $options; - } - - /** - * @param string $formatter One of "lessjs", "compressed", or "classic". - */ - public function setFormatter($formatter) - { - $this->formatter = $formatter; - } - - /** - * @param boolean $preserveComments - */ - public function setPreserveComments($preserveComments) - { - $this->preserveComments = $preserveComments; - } - - public function filterLoad(AssetInterface $asset) - { - $lc = new \lessc(); - if ($dir = $asset->getSourceDirectory()) { - $lc->importDir = $dir; - } - - foreach ($this->loadPaths as $loadPath) { - $lc->addImportDir($loadPath); - } - - foreach ($this->customFunctions as $name => $callable) { - $lc->registerFunction($name, $callable); - } - - if ($this->formatter) { - $lc->setFormatter($this->formatter); - } - - if (null !== $this->preserveComments) { - $lc->setPreserveComments($this->preserveComments); - } - - if (method_exists($lc, 'setOptions') && count($this->options) > 0) { - $lc->setOptions($this->options); - } - - $asset->setContent($lc->parse($asset->getContent(), $this->presets)); - } - - public function registerFunction($name, $callable) - { - $this->customFunctions[$name] = $callable; - } - - public function filterDump(AssetInterface $asset) - { - } - - public function getChildren(AssetFactory $factory, $content, $loadPath = null) - { - $loadPaths = $this->loadPaths; - if (null !== $loadPath) { - $loadPaths[] = $loadPath; - } - - if (empty($loadPaths)) { - return array(); - } - - $children = array(); - foreach (LessUtils::extractImports($content) as $reference) { - if ('.css' === substr($reference, -4)) { - // skip normal css imports - // todo: skip imports with media queries - continue; - } - - if ('.less' !== substr($reference, -5)) { - $reference .= '.less'; - } - - foreach ($loadPaths as $loadPath) { - if (file_exists($file = $loadPath.'/'.$reference)) { - $coll = $factory->createAsset($file, array(), array('root' => $loadPath)); - foreach ($coll as $leaf) { - $leaf->ensureFilter($this); - $children[] = $leaf; - goto next_reference; - } - } - } - - next_reference: - } - - return $children; - } -} diff --git a/src/Assetic/Filter/MinifyCssCompressorFilter.php b/src/Assetic/Filter/MinifyCssCompressorFilter.php deleted file mode 100644 index acac575eb..000000000 --- a/src/Assetic/Filter/MinifyCssCompressorFilter.php +++ /dev/null @@ -1,33 +0,0 @@ - - * @author http://code.google.com/u/1stvamp/ (Issue 64 patch) - */ -class MinifyCssCompressorFilter implements FilterInterface -{ - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - $asset->setContent(\Minify_CSS_Compressor::process($asset->getContent())); - } -} diff --git a/src/Assetic/Filter/PackagerFilter.php b/src/Assetic/Filter/PackagerFilter.php deleted file mode 100644 index e65348f40..000000000 --- a/src/Assetic/Filter/PackagerFilter.php +++ /dev/null @@ -1,63 +0,0 @@ - - */ -class PackagerFilter implements FilterInterface -{ - private $packages; - - public function __construct(array $packages = array()) - { - $this->packages = $packages; - } - - public function addPackage($package) - { - $this->packages[] = $package; - } - - public function filterLoad(AssetInterface $asset) - { - static $manifest = <<getContent()); - - $packager = new \Packager(array_merge(array($package), $this->packages)); - $content = $packager->build(array(), array(), array('Application'.$hash)); - - unlink($package.'/package.yml'); - unlink($package.'/source.js'); - rmdir($package); - - $asset->setContent($content); - } - - public function filterDump(AssetInterface $asset) - { - } -} diff --git a/src/Assetic/Filter/PackerFilter.php b/src/Assetic/Filter/PackerFilter.php deleted file mode 100644 index b6dcc5aa8..000000000 --- a/src/Assetic/Filter/PackerFilter.php +++ /dev/null @@ -1,54 +0,0 @@ - - */ -class PackerFilter implements FilterInterface -{ - protected $encoding = 'None'; - - protected $fastDecode = true; - - protected $specialChars = false; - - public function setEncoding($encoding) - { - $this->encoding = $encoding; - } - - public function setFastDecode($fastDecode) - { - $this->fastDecode = (bool) $fastDecode; - } - - public function setSpecialChars($specialChars) - { - $this->specialChars = (bool) $specialChars; - } - - public function filterLoad(AssetInterface $asset) - { - } - - public function filterDump(AssetInterface $asset) - { - $packer = new \JavaScriptPacker($asset->getContent(), $this->encoding, $this->fastDecode, $this->specialChars); - $asset->setContent($packer->pack()); - } -} diff --git a/src/Assetic/Filter/ScssphpFilter.php b/src/Assetic/Filter/ScssphpFilter.php deleted file mode 100644 index c2cefeff9..000000000 --- a/src/Assetic/Filter/ScssphpFilter.php +++ /dev/null @@ -1,146 +0,0 @@ - - */ -class ScssphpFilter implements DependencyExtractorInterface -{ - private $compass = false; - private $importPaths = array(); - private $customFunctions = array(); - private $formatter; - private $variables = array(); - - public function enableCompass($enable = true) - { - $this->compass = (Boolean) $enable; - } - - public function isCompassEnabled() - { - return $this->compass; - } - - public function setFormatter($formatter) - { - $legacyFormatters = array( - 'scss_formatter' => 'ScssPhp\ScssPhp\Formatter\Expanded', - 'scss_formatter_nested' => 'ScssPhp\ScssPhp\Formatter\Nested', - 'scss_formatter_compressed' => 'ScssPhp\ScssPhp\Formatter\Compressed', - 'scss_formatter_crunched' => 'ScssPhp\ScssPhp\Formatter\Crunched', - ); - - if (isset($legacyFormatters[$formatter])) { - @trigger_error(sprintf('The scssphp formatter `%s` is deprecated. Use `%s` instead.', $formatter, $legacyFormatters[$formatter]), E_USER_DEPRECATED); - - $formatter = $legacyFormatters[$formatter]; - } - - $this->formatter = $formatter; - } - - public function setVariables(array $variables) - { - $this->variables = $variables; - } - - public function addVariable($variable) - { - $this->variables[] = $variable; - } - - public function setImportPaths(array $paths) - { - $this->importPaths = $paths; - } - - public function addImportPath($path) - { - $this->importPaths[] = $path; - } - - public function registerFunction($name, $callable) - { - $this->customFunctions[$name] = $callable; - } - - public function filterLoad(AssetInterface $asset) - { - $sc = new Compiler(); - - if ($this->compass) { - new \scss_compass($sc); - } - - if ($dir = $asset->getSourceDirectory()) { - $sc->addImportPath($dir); - } - - foreach ($this->importPaths as $path) { - $sc->addImportPath($path); - } - - foreach ($this->customFunctions as $name => $callable) { - $sc->registerFunction($name, $callable); - } - - if ($this->formatter) { - $sc->setFormatter($this->formatter); - } - - if (!empty($this->variables)) { - $sc->setVariables($this->variables); - } - - $asset->setContent($sc->compile($asset->getContent())); - } - - public function filterDump(AssetInterface $asset) - { - } - - public function getChildren(AssetFactory $factory, $content, $loadPath = null) - { - $sc = new Compiler(); - if ($loadPath !== null) { - $sc->addImportPath($loadPath); - } - - foreach ($this->importPaths as $path) { - $sc->addImportPath($path); - } - - $children = array(); - foreach (CssUtils::extractImports($content) as $match) { - $file = $sc->findImport($match); - if ($file) { - $children[] = $child = $factory->createAsset($file, [], ['root' => $loadPath]); - $child->load(); - $childLoadPath = $child->all()[0]->getSourceDirectory(); - $children = array_merge($children, $this->getChildren($factory, $child->getContent(), $childLoadPath)); - } - } - - return $children; - } -} diff --git a/src/Assetic/Filter/StylesheetMinify.php b/src/Assetic/Filter/StylesheetMinify.php deleted file mode 100644 index f8cd23c7b..000000000 --- a/src/Assetic/Filter/StylesheetMinify.php +++ /dev/null @@ -1,63 +0,0 @@ -setContent($this->minify($asset->getContent())); - } - - /** - * Minifies CSS - * @var $css string CSS code to minify. - * @return string Minified CSS. - */ - protected function minify($css) - { - // Normalize whitespace in a smart way - $css = preg_replace('/\s{2,}/', ' ', $css); - - // Remove spaces before and after comment - $css = preg_replace('/(\s+)(\/\*[^!](.*?)\*\/)(\s+)/', '$2', $css); - - // Remove comment blocks, everything between /* and */, ignore /*! comments - $css = preg_replace('#/\*[^\!].*?\*/#s', '', $css); - - // Remove ; before } - $css = preg_replace('/;(?=\s*})/', '', $css); - - // Remove space after , : ; { } */ >, but not after !*/ - $css = preg_replace('/(,|:|;|\{|}|[^!]\*\/|>) /', '$1', $css); - - // Remove space before , ; { } > - $css = preg_replace('/ (,|;|\{|}|>)/', '$1', $css); - - // Remove newline before } > - $css = preg_replace('/(\r\n|\r|\n)(})/', '$2', $css); - - // Remove trailing zeros from float numbers preceded by : or a white-space - // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px - $css = preg_replace('/((? - */ -class FilterManager -{ - private $filters = array(); - - public function set($alias, FilterInterface $filter) - { - $this->checkName($alias); - - $this->filters[$alias] = $filter; - } - - public function get($alias) - { - if (!isset($this->filters[$alias])) { - throw new \InvalidArgumentException(sprintf('There is no "%s" filter.', $alias)); - } - - return $this->filters[$alias]; - } - - public function has($alias) - { - return isset($this->filters[$alias]); - } - - public function getNames() - { - return array_keys($this->filters); - } - - /** - * Checks that a name is valid. - * - * @param string $name An asset name candidate - * - * @throws \InvalidArgumentException If the asset name is invalid - */ - protected function checkName($name) - { - if (!ctype_alnum(str_replace('_', '', $name))) { - throw new \InvalidArgumentException(sprintf('The name "%s" is invalid.', $name)); - } - } -} diff --git a/src/Assetic/LICENSE b/src/Assetic/LICENSE deleted file mode 100644 index f5dedf447..000000000 --- a/src/Assetic/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2010-2015 OpenSky Project Inc - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is furnished -to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/src/Assetic/README.md b/src/Assetic/README.md deleted file mode 100644 index 2c98ad06c..000000000 --- a/src/Assetic/README.md +++ /dev/null @@ -1,346 +0,0 @@ -# Storm Assetic Resources - -These libraries are useful when parsing assets with the Assetic combiner. - -Assetic is an asset management framework for PHP. - -``` php -dump(); -``` - -Assets ------- - -An Assetic asset is something with filterable content that can be loaded and -dumped. An asset also includes metadata, some of which can be manipulated and -some of which is immutable. - -| **Property** | **Accessor** | **Mutator** | -|--------------|-----------------|---------------| -| content | getContent | setContent | -| mtime | getLastModified | n/a | -| source root | getSourceRoot | n/a | -| source path | getSourcePath | n/a | -| target path | getTargetPath | setTargetPath | - -The "target path" property denotes where an asset (or an collection of assets) should be dumped. - -Filters -------- - -Filters can be applied to manipulate assets. - -``` php -dump(); -``` - -The filters applied to the collection will cascade to each asset leaf if you -iterate over it. - -``` php -dump(); -} -``` - -The core provides the following filters in the `Assetic\Filter` namespace: - - * `AutoprefixerFilter`: Parse and update vendor-specific properties using autoprefixer - * `CoffeeScriptFilter`: compiles CoffeeScript into Javascript - * `CompassFilter`: Compass CSS authoring framework - * `CssEmbedFilter`: embeds image data in your stylesheets - * `CssImportFilter`: inlines imported stylesheets - * `CssMinFilter`: minifies CSS - * `CleanCssFilter`: minifies CSS - * `CssRewriteFilter`: fixes relative URLs in CSS assets when moving to a new URL - * `DartFilter`: compiles Javascript using dart2js - * `EmberPrecompileFilter`: precompiles Handlebars templates into Javascript for use in the Ember.js framework - * `GoogleClosure\CompilerApiFilter`: compiles Javascript using the Google Closure Compiler API - * `GoogleClosure\CompilerJarFilter`: compiles Javascript using the Google Closure Compiler JAR - * `GssFilter`: compliles CSS using the Google Closure Stylesheets Compiler - * `HandlebarsFilter`: compiles Handlebars templates into Javascript - * `JpegoptimFilter`: optimize your JPEGs - * `JpegtranFilter`: optimize your JPEGs - * `JSMinFilter`: minifies Javascript - * `JSMinPlusFilter`: minifies Javascript - * `JSqueezeFilter`: compresses Javascript - * `LessFilter`: parses LESS into CSS (using less.js with node.js) - * `LessphpFilter`: parses LESS into CSS (using lessphp) - * `OptiPngFilter`: optimize your PNGs - * `PackerFilter`: compresses Javascript using Dean Edwards's Packer - * `PhpCssEmbedFilter`: embeds image data in your stylesheet - * `PngoutFilter`: optimize your PNGs - * `ReactJsxFilter`: compiles React JSX into JavaScript - * `Sass\SassFilter`: parses SASS into CSS - * `Sass\ScssFilter`: parses SCSS into CSS - * `SassphpFilter`: parses Sass into CSS using the sassphp bindings for Libsass - * `ScssphpFilter`: parses SCSS using scssphp - * `SeparatorFilter`: inserts a separator between assets to prevent merge failures - * `SprocketsFilter`: Sprockets Javascript dependency management - * `StylusFilter`: parses STYL into CSS - * `TypeScriptFilter`: parses TypeScript into Javascript - * `UglifyCssFilter`: minifies CSS - * `UglifyJs2Filter`: minifies Javascript - * `UglifyJsFilter`: minifies Javascript - * `Yui\CssCompressorFilter`: compresses CSS using the YUI compressor - * `Yui\JsCompressorFilter`: compresses Javascript using the YUI compressor - -Asset Manager -------------- - -An asset manager is provided for organizing assets. - -``` php -set('jquery', new FileAsset('/path/to/jquery.js')); -$am->set('base_css', new GlobAsset('/path/to/css/*')); -``` - -The asset manager can also be used to reference assets to avoid duplication. - -``` php -set('my_plugin', new AssetCollection(array( - new AssetReference($am, 'jquery'), - new FileAsset('/path/to/jquery.plugin.js'), -))); -``` - -Filter Manager --------------- - -A filter manager is also provided for organizing filters. - -``` php -set('sass', new SassFilter('/path/to/parser/sass')); -$fm->set('yui_css', new Yui\CssCompressorFilter('/path/to/yuicompressor.jar')); -``` - -Asset Factory -------------- - -If you'd rather not create all these objects by hand, you can use the asset -factory, which will do most of the work for you. - -``` php -setAssetManager($am); -$factory->setFilterManager($fm); -$factory->setDebug(true); - -$css = $factory->createAsset(array( - '@reset', // load the asset manager's "reset" asset - 'css/src/*.scss', // load every scss files from "/path/to/asset/directory/css/src/" -), array( - 'scss', // filter through the filter manager's "scss" filter - '?yui_css', // don't use this filter in debug mode -)); - -echo $css->dump(); -``` - -The `AssetFactory` is constructed with a root directory which is used as the base directory for relative asset paths. - -Prefixing a filter name with a question mark, as `yui_css` is here, will cause -that filter to be omitted when the factory is in debug mode. - -You can also register [Workers](src/Assetic/Factory/Worker/WorkerInterface.php) on the factory and all assets created -by it will be passed to the worker's `process()` method before being returned. See _Cache Busting_ below for an example. - -Dumping Assets to static files ------------------------------- - -You can dump all the assets an AssetManager holds to files in a directory. This will probably be below your webserver's document root -so the files can be served statically. - -``` php -writeManagerAssets($am); -``` - -This will make use of the assets' target path. - -Cache Busting -------------- - -If you serve your assets from static files as just described, you can use the CacheBustingWorker to rewrite the target -paths for assets. It will insert an identifier before the filename extension that is unique for a particular version -of the asset. - -This identifier is based on the modification time of the asset and will also take depended-on assets into -consideration if the applied filters support it. - -``` php -setAssetManager($am); -$factory->setFilterManager($fm); -$factory->setDebug(true); -$factory->addWorker(new CacheBustingWorker()); - -$css = $factory->createAsset(array( - '@reset', // load the asset manager's "reset" asset - 'css/src/*.scss', // load every scss files from "/path/to/asset/directory/css/src/" -), array( - 'scss', // filter through the filter manager's "scss" filter - '?yui_css', // don't use this filter in debug mode -)); - -echo $css->dump(); -``` - -Internal caching -------- - -A simple caching mechanism is provided to avoid unnecessary work. - -``` php -dump(); -$js->dump(); -$js->dump(); -``` - -Twig ----- - -To use the Assetic [Twig][3] extension you must register it to your Twig -environment: - -``` php -addExtension(new AsseticExtension($factory)); -``` - -Once in place, the extension exposes a stylesheets and a javascripts tag with a syntax similar -to what the asset factory uses: - -``` html+jinja -{% stylesheets '/path/to/sass/main.sass' filter='sass,?yui_css' output='css/all.css' %} - -{% endstylesheets %} -``` - -This example will render one `link` element on the page that includes a URL -where the filtered asset can be found. - -When the extension is in debug mode, this same tag will render multiple `link` -elements, one for each asset referenced by the `css/src/*.sass` glob. The -specified filters will still be applied, unless they are marked as optional -using the `?` prefix. - -This behavior can also be triggered by setting a `debug` attribute on the tag: - -``` html+jinja -{% stylesheets 'css/*' debug=true %} ... {% stylesheets %} -``` - -These assets need to be written to the web directory so these URLs don't -return 404 errors. - -``` php -setLoader('twig', new TwigFormulaLoader($twig)); - -// loop through all your templates -foreach ($templates as $template) { - $resource = new TwigResource($twigLoader, $template); - $am->addResource($resource, 'twig'); -} - -$writer = new AssetWriter('/path/to/web'); -$writer->writeManagerAssets($am); -``` - ---- - -Assetic is based on the Python [webassets][1] library (available on -[GitHub][2]). - -[1]: http://elsdoerfer.name/docs/webassets -[2]: https://github.com/miracle2k/webassets -[3]: http://twig.sensiolabs.org diff --git a/src/Assetic/Util/CssUtils.php b/src/Assetic/Util/CssUtils.php deleted file mode 100644 index 229914cde..000000000 --- a/src/Assetic/Util/CssUtils.php +++ /dev/null @@ -1,136 +0,0 @@ - - */ -abstract class CssUtils -{ - const REGEX_URLS = '/url\((["\']?)(?P.*?)(\\1)\)/'; - const REGEX_IMPORTS = '/@import (?:url\()?(\'|"|)(?P[^\'"\)\n\r]*)\1\)?;?/'; - const REGEX_IMPORTS_NO_URLS = '/@import (?!url\()(\'|"|)(?P[^\'"\)\n\r]*)\1;?/'; - const REGEX_IE_FILTERS = '/src=(["\']?)(?P.*?)\\1/'; - const REGEX_COMMENTS = '/(\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)/'; - - /** - * Filters all references -- url() and "@import" -- through a callable. - * - * @param string $content The CSS - * @param callable $callback A PHP callable - * - * @return string The filtered CSS - */ - public static function filterReferences($content, $callback) - { - $content = static::filterUrls($content, $callback); - $content = static::filterImports($content, $callback, false); - $content = static::filterIEFilters($content, $callback); - - return $content; - } - - /** - * Filters all CSS url()'s through a callable. - * - * @param string $content The CSS - * @param callable $callback A PHP callable - * - * @return string The filtered CSS - */ - public static function filterUrls($content, $callback) - { - $pattern = static::REGEX_URLS; - - return static::filterCommentless($content, function ($part) use (&$callback, $pattern) { - return preg_replace_callback($pattern, $callback, $part); - }); - } - - /** - * Filters all CSS imports through a callable. - * - * @param string $content The CSS - * @param callable $callback A PHP callable - * @param Boolean $includeUrl Whether to include url() in the pattern - * - * @return string The filtered CSS - */ - public static function filterImports($content, $callback, $includeUrl = true) - { - $pattern = $includeUrl ? static::REGEX_IMPORTS : static::REGEX_IMPORTS_NO_URLS; - - return static::filterCommentless($content, function ($part) use (&$callback, $pattern) { - return preg_replace_callback($pattern, $callback, $part); - }); - } - - /** - * Filters all IE filters (AlphaImageLoader filter) through a callable. - * - * @param string $content The CSS - * @param callable $callback A PHP callable - * - * @return string The filtered CSS - */ - public static function filterIEFilters($content, $callback) - { - $pattern = static::REGEX_IE_FILTERS; - - return static::filterCommentless($content, function ($part) use (&$callback, $pattern) { - return preg_replace_callback($pattern, $callback, $part); - }); - } - - /** - * Filters each non-comment part through a callable. - * - * @param string $content The CSS - * @param callable $callback A PHP callable - * - * @return string The filtered CSS - */ - public static function filterCommentless($content, $callback) - { - $result = ''; - foreach (preg_split(static::REGEX_COMMENTS, $content, -1, PREG_SPLIT_DELIM_CAPTURE) as $part) { - if (!preg_match(static::REGEX_COMMENTS, $part, $match) || $part != $match[0]) { - $part = call_user_func($callback, $part); - } - - $result .= $part; - } - - return $result; - } - - /** - * Extracts all references from the supplied CSS content. - * - * @param string $content The CSS content - * - * @return array An array of unique URLs - */ - public static function extractImports($content) - { - $imports = array(); - static::filterImports($content, function ($matches) use (&$imports) { - $imports[] = $matches['url']; - }); - - return array_unique(array_filter($imports)); - } - - final private function __construct() - { - } -} diff --git a/src/Assetic/Util/FilesystemUtils.php b/src/Assetic/Util/FilesystemUtils.php deleted file mode 100644 index bc8395a67..000000000 --- a/src/Assetic/Util/FilesystemUtils.php +++ /dev/null @@ -1,82 +0,0 @@ - - */ -class FilesystemUtils -{ - /** - * Recursively removes a directory from the filesystem. - */ - public static function removeDirectory($directory) - { - $inner = new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS); - $outer = new \RecursiveIteratorIterator($inner, \RecursiveIteratorIterator::SELF_FIRST); - - // remove the files first - foreach ($outer as $file) { - if ($file->isFile()) { - unlink($file); - } - } - - // remove the sub-directories next - $files = iterator_to_array($outer); - foreach (array_reverse($files) as $file) { - /** @var \SplFileInfo $file */ - if ($file->isDir()) { - rmdir($file); - } - } - - // finally the directory itself - rmdir($directory); - } - - /** - * Creates a throw-away directory. - * - * This is not considered a "temporary" directory because it will not be - * automatically deleted at the end of the request or process. It must be - * deleted manually. - * - * @param string $prefix A prefix for the directory name - * - * @return string The directory path - */ - public static function createThrowAwayDirectory($prefix) - { - $directory = self::getTemporaryDirectory().DIRECTORY_SEPARATOR.uniqid('assetic_'.$prefix); - mkdir($directory); - - return $directory; - } - - /** - * Creates a temporary file. - * - * @param string $prefix A prefix for the file name - * - * @return string The file path - */ - public static function createTemporaryFile($prefix) - { - return tempnam(self::getTemporaryDirectory(), 'assetic_'.$prefix); - } - - public static function getTemporaryDirectory() - { - return realpath(sys_get_temp_dir()); - } -} diff --git a/src/Assetic/Util/LessUtils.php b/src/Assetic/Util/LessUtils.php deleted file mode 100644 index 25f4574d0..000000000 --- a/src/Assetic/Util/LessUtils.php +++ /dev/null @@ -1,22 +0,0 @@ - - */ -abstract class LessUtils extends CssUtils -{ - const REGEX_IMPORTS = '/@import(?:-once)? (?:\([a-z]*\) )?(?:url\()?(\'|"|)(?P[^\'"\)\n\r]*)\1\)?;?/'; - const REGEX_IMPORTS_NO_URLS = '/@import(?:-once)? (?:\([a-z]*\) )?(?!url\()(\'|"|)(?P[^\'"\)\n\r]*)\1;?/'; - const REGEX_COMMENTS = '/((?:\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)|\/\/[^\n]+)/'; -} diff --git a/src/Assetic/Util/SassUtils.php b/src/Assetic/Util/SassUtils.php deleted file mode 100644 index 175221415..000000000 --- a/src/Assetic/Util/SassUtils.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ -abstract class SassUtils extends CssUtils -{ - const REGEX_COMMENTS = '/((?:\/\*[^*]*\*+(?:[^\/][^*]*\*+)*\/)|\/\/[^\n]+)/'; -} diff --git a/src/Assetic/Util/TraversableString.php b/src/Assetic/Util/TraversableString.php deleted file mode 100644 index 5a5020af7..000000000 --- a/src/Assetic/Util/TraversableString.php +++ /dev/null @@ -1,42 +0,0 @@ - - */ -class TraversableString implements \IteratorAggregate, \Countable -{ - private $one; - private $many; - - public function __construct($one, array $many) - { - $this->one = $one; - $this->many = $many; - } - - public function getIterator() - { - return new \ArrayIterator($this->many); - } - - public function count() - { - return count($this->many); - } - - public function __toString() - { - return (string) $this->one; - } -} diff --git a/src/Assetic/Util/VarUtils.php b/src/Assetic/Util/VarUtils.php deleted file mode 100644 index e3a3d2ea7..000000000 --- a/src/Assetic/Util/VarUtils.php +++ /dev/null @@ -1,82 +0,0 @@ - - */ -abstract class VarUtils -{ - /** - * Resolves variable placeholders. - * - * @param string $template A template string - * @param array $vars Variable names - * @param array $values Variable values - * - * @return string The resolved string - * - * @throws \InvalidArgumentException If there is a variable with no value - */ - public static function resolve($template, array $vars, array $values) - { - $map = array(); - foreach ($vars as $var) { - if (false === strpos($template, '{'.$var.'}')) { - continue; - } - - if (!isset($values[$var])) { - throw new \InvalidArgumentException(sprintf('The template "%s" contains the variable "%s", but was not given any value for it.', $template, $var)); - } - - $map['{'.$var.'}'] = $values[$var]; - } - - return strtr($template, $map); - } - - public static function getCombinations(array $vars, array $values) - { - if (!$vars) { - return array(array()); - } - - $combinations = array(); - $nbValues = array(); - foreach ($values as $var => $vals) { - if (!in_array($var, $vars, true)) { - continue; - } - - $nbValues[$var] = count($vals); - } - - for ($i = array_product($nbValues), $c = $i * 2; $i < $c; $i++) { - $k = $i; - $combination = array(); - - foreach ($vars as $var) { - $combination[$var] = $values[$var][$k % $nbValues[$var]]; - $k = intval($k / $nbValues[$var]); - } - - $combinations[] = $combination; - } - - return $combinations; - } - - final private function __construct() - { - } -} diff --git a/src/Auth/AuthenticationException.php b/src/Auth/AuthenticationException.php index 21c473f64..55ec3b449 100644 --- a/src/Auth/AuthenticationException.php +++ b/src/Auth/AuthenticationException.php @@ -1,9 +1,9 @@ createUserModel(); + /** @var \Winter\Storm\Database\Builder */ $query = $model->newQuery(); $this->extendUserQuery($query); @@ -139,6 +140,7 @@ public function register(array $credentials, $activate = false, $autoLogin = tru // Prevents revalidation of the password field // on subsequent saves to this model object + /** @phpstan-ignore-next-line */ $user->password = null; if ($autoLogin) { @@ -148,8 +150,18 @@ public function register(array $credentials, $activate = false, $autoLogin = tru return $user; } + /** + * Determine if the guard has a user instance. + * @return bool + */ + public function hasUser() + { + return isset($this->user); + } + /** * Sets the user + * @phpstan-param Models\User $user */ public function setUser(Authenticatable $user) { @@ -235,6 +247,7 @@ public function findUserByCredentials(array $credentials) } } + /** @var Models\User */ $user = $query->first(); if (!$this->validateUserModel($user)) { throw new AuthenticationException('A user was not found with the given credentials.'); @@ -329,7 +342,10 @@ public function findThrottleByUserId($userId, $ipAddress = null) }); } - if (!$throttle = $query->first()) { + /** @var Models\Throttle|null */ + $throttle = $query->first(); + + if (!$throttle) { $throttle = $this->createThrottleModel(); $throttle->user_id = $userId; if ($ipAddress) { @@ -352,7 +368,7 @@ public function findThrottleByUserId($userId, $ipAddress = null) * @param array $credentials The user login details * @param bool $remember Store a non-expire cookie for the user * @throws AuthenticationException If authentication fails - * @return Models\User The successfully logged in user + * @return bool If authentication was successful */ public function attempt(array $credentials = [], $remember = false) { @@ -374,7 +390,7 @@ public function validate(array $credentials = []) * Validate a user's credentials, method used internally. * * @param array $credentials - * @return User + * @return Models\User|null */ protected function validateInternal(array $credentials = []) { @@ -404,7 +420,9 @@ protected function validateInternal(array $credentials = []) /* * If throttling is enabled, check they are not locked out first and foremost. */ - if ($this->useThrottle) { + $useThrottle = $this->useThrottle; + + if ($useThrottle) { $throttle = $this->findThrottleByLogin($credentials[$loginName], $this->ipAddress); $throttle->check(); } @@ -416,14 +434,15 @@ protected function validateInternal(array $credentials = []) $user = $this->findUserByCredentials($credentials); } catch (AuthenticationException $ex) { - if ($this->useThrottle) { + if ($useThrottle) { $throttle->addLoginAttempt(); } + $user = null; throw $ex; } - if ($this->useThrottle) { + if ($useThrottle) { $throttle->clearLoginAttempts(); } @@ -612,6 +631,7 @@ public function onceUsingId($id) * Logs in the given user and sets properties * in the session. * @throws AuthenticationException If the user is not activated and $this->requireActivation = true + * @phpstan-param Models\User $user */ public function login(Authenticatable $user, $remember = true) { @@ -647,7 +667,7 @@ public function login(Authenticatable $user, $remember = true) * * @param mixed $id * @param bool $remember - * @return \Illuminate\Contracts\Auth\Authenticatable + * @return \Illuminate\Contracts\Auth\Authenticatable|false */ public function loginUsingId($id, $remember = false) { @@ -705,7 +725,7 @@ public function logout() * Impersonates the given user and sets properties in the session but not the cookie. * * @param Models\User $impersonatee - * @throws Exception If the current user is not permitted to impersonate the provided user + * @throws AuthorizationException If the current user is not permitted to impersonate the provided user * @return void */ public function impersonate($impersonatee) @@ -819,7 +839,7 @@ public function isImpersonator() /** * Get the original user doing the impersonation * - * @return mixed Returns the User model for the impersonator if able, false if not + * @return Models\User|false Returns the User model for the impersonator if able, `false` if not */ public function getImpersonator() { @@ -836,7 +856,10 @@ public function getImpersonator() return $this->impersonator; } - return $this->impersonator = $this->createUserModel()->find($impersonatorId); + /** @var Models\User|false */ + $impersonator = $this->createUserModel()->find($impersonatorId) ?? false; + + return $this->impersonator = $impersonator; } /** diff --git a/src/Auth/Migrations/2013_10_01_000001_Db_Users.php b/src/Auth/Migrations/2013_10_01_000001_Db_Users.php index 2d957959b..b09769518 100644 --- a/src/Auth/Migrations/2013_10_01_000001_Db_Users.php +++ b/src/Auth/Migrations/2013_10_01_000001_Db_Users.php @@ -23,6 +23,7 @@ public function up() $table->timestamp('activated_at')->nullable(); $table->timestamp('last_login')->nullable(); $table->integer('role_id')->unsigned()->nullable()->index(); + $table->boolean('is_superuser')->default(0); $table->timestamps(); }); } diff --git a/src/Auth/Models/Group.php b/src/Auth/Models/Group.php index 21d5e06cb..2c3da8fa9 100644 --- a/src/Auth/Models/Group.php +++ b/src/Auth/Models/Group.php @@ -4,6 +4,8 @@ /** * Group model + * + * @method \Winter\Storm\Database\Relations\BelongsToMany users() Users relation. */ class Group extends Model { @@ -29,7 +31,7 @@ class Group extends Model ]; /** - * @var array The attributes that aren't mass assignable. + * @var string[]|bool The attributes that aren't mass assignable. */ protected $guarded = []; diff --git a/src/Auth/Models/Preferences.php b/src/Auth/Models/Preferences.php index a24537789..8e21119e1 100644 --- a/src/Auth/Models/Preferences.php +++ b/src/Auth/Models/Preferences.php @@ -6,6 +6,15 @@ /** * User Preferences model + * + * @property string|array|null $value Represents the value of the preference. + * @property string|null $namespace Represents the namespace of the preference. + * @property string|null $group Represents the group of the preference. + * @property string|null $item Represents the item name of the preference. + * @property int|null $user_id Represents the user ID that this preference belongs to. + * + * @method static \Winter\Storm\Database\QueryBuilder applyKeyAndUser($key, $user = null) Scope to find a setting record + * for the specified module (or plugin) name, setting name and user. */ class Preferences extends Model { @@ -26,7 +35,7 @@ class Preferences extends Model protected $jsonable = ['value']; /** - * @var \Winter\Storm\Auth\Models\User A user who owns the preferences + * @var \Winter\Storm\Auth\Models\User|null A user who owns the preferences */ public $userContext; @@ -38,6 +47,7 @@ class Preferences extends Model public function resolveUser($user) { $user = Manager::instance()->getUser(); + if (!$user) { throw new AuthException('User is not logged in'); } @@ -63,7 +73,9 @@ public static function forUser($user = null) */ public function get($key, $default = null) { - if (!($user = $this->userContext)) { + $user = $this->userContext; + + if (!$user) { return $default; } @@ -74,6 +86,7 @@ public function get($key, $default = null) } $record = static::findRecord($key, $user); + if (!$record) { return static::$cache[$cacheKey] = $default; } @@ -91,11 +104,14 @@ public function get($key, $default = null) */ public function set($key, $value) { - if (!$user = $this->userContext) { + $user = $this->userContext; + + if (!$user) { return false; } $record = static::findRecord($key, $user); + if (!$record) { list($namespace, $group, $item) = $this->parseKey($key); $record = new static; @@ -120,11 +136,14 @@ public function set($key, $value) */ public function reset($key) { - if (!$user = $this->userContext) { + $user = $this->userContext; + + if (!$user) { return false; } $record = static::findRecord($key, $user); + if (!$record) { return false; } @@ -139,7 +158,7 @@ public function reset($key) /** * Returns a record - * @return self + * @return self|null */ public static function findRecord($key, $user = null) { @@ -148,8 +167,8 @@ public static function findRecord($key, $user = null) /** * Scope to find a setting record for the specified module (or plugin) name, setting name and user. + * @param \Winter\Storm\Database\QueryBuilder $query * @param string $key Specifies the setting key value, for example 'backend:items.perpage' - * @param mixed $default The default value to return if the setting doesn't exist in the DB. * @param mixed $user An optional user object. * @return mixed Returns the found record or null. */ diff --git a/src/Auth/Models/Role.php b/src/Auth/Models/Role.php index c1781326b..efa8f3359 100644 --- a/src/Auth/Models/Role.php +++ b/src/Auth/Models/Role.php @@ -5,6 +5,8 @@ /** * Role model + * + * @property array $permissions Permissions array. */ class Role extends Model { @@ -44,7 +46,7 @@ class Role extends Model protected $allowedPermissionsValues = [0, 1]; /** - * @var array The attributes that aren't mass assignable. + * @var string[]|bool The attributes that aren't mass assignable. */ protected $guarded = []; @@ -167,6 +169,7 @@ public function hasAnyAccess(array $permissions) public function setPermissionsAttribute($permissions) { $permissions = json_decode($permissions, true); + foreach ($permissions as $permission => $value) { if (!in_array($value = (int) $value, $this->allowedPermissionsValues)) { throw new InvalidArgumentException(sprintf( diff --git a/src/Auth/Models/Throttle.php b/src/Auth/Models/Throttle.php index cfa4c16d0..a4cdbea58 100644 --- a/src/Auth/Models/Throttle.php +++ b/src/Auth/Models/Throttle.php @@ -6,6 +6,9 @@ /** * Throttle model + * + * @property \Winter\Storm\Auth\Models\User|null $user Related user. + * @method \Winter\Storm\Database\Relations\BelongsTo user() User relation. */ class Throttle extends Model { @@ -50,7 +53,7 @@ class Throttle extends Model /** * Returns the associated user with the throttler. - * @return User + * @return \Illuminate\Database\Eloquent\Model|null */ public function getUser() { diff --git a/src/Auth/Models/User.php b/src/Auth/Models/User.php index fe22d414b..8e7f020ad 100644 --- a/src/Auth/Models/User.php +++ b/src/Auth/Models/User.php @@ -1,13 +1,19 @@ The attributes that should be hidden for arrays. */ protected $hidden = ['password', 'reset_password_code', 'activation_code', 'persist_code']; /** - * @var array The attributes that aren't mass assignable. + * @var string[]|bool The attributes that aren't mass assignable. */ protected $guarded = ['is_superuser', 'reset_password_code', 'activation_code', 'persist_code', 'role_id']; @@ -150,7 +156,7 @@ public function afterLogin() /** * Delete the user groups - * @return bool + * @return void */ public function afterDelete() { @@ -341,7 +347,7 @@ public function getGroups() /** * Returns the role assigned to this user. - * @return Winter\Storm\Auth\Models\Role + * @return \Winter\Storm\Auth\Models\Role|null */ public function getRole() { @@ -404,8 +410,9 @@ public function getMergedPermissions() { if (!$this->mergedPermissions) { $permissions = []; + $role = $this->getRole(); - if (($role = $this->getRole()) && is_array($role->permissions)) { + if ($role && is_array($role->permissions)) { $permissions = array_merge($permissions, $role->permissions); } @@ -564,6 +571,7 @@ public function hasAnyAccess(array $permissions) public function setPermissionsAttribute($permissions) { $permissions = json_decode($permissions, true) ?: []; + foreach ($permissions as $permission => &$value) { if (!in_array($value = (int) $value, $this->allowedPermissionsValues)) { throw new InvalidArgumentException(sprintf( @@ -632,7 +640,7 @@ public function getRememberToken() /** * Set the token value for the "remember me" session. - * @param string $value + * @param string|null $value * @return void */ public function setRememberToken($value) diff --git a/src/Config/ConfigWriter.php b/src/Config/ConfigWriter.php index 9761b1cba..75f40831a 100644 --- a/src/Config/ConfigWriter.php +++ b/src/Config/ConfigWriter.php @@ -2,217 +2,49 @@ use Exception; +use PhpParser\Error; +use PhpParser\Lexer\Emulative; +use PhpParser\ParserFactory; +use Winter\Storm\Exception\SystemException; +use Winter\Storm\Parse\PHP\ArrayFile; + /** * Configuration rewriter * - * https://github.com/daftspunk/laravel-config-writer + * @see https://wintercms.com/docs/services/parser#data-file-array * * This class lets you rewrite array values inside a basic configuration file * that returns a single array definition (a Laravel config file) whilst maintaining * the integrity of the file, leaving comments and advanced settings intact. - * - * The following value types are supported for writing: - * - strings - * - integers - * - booleans - * - nulls - * - single-dimension arrays - * - * To do: - * - When an entry does not exist, provide a way to create it - * - * Pro Regextip: Use [\s\S] instead of . for multiline support */ class ConfigWriter { - public function toFile($filePath, $newValues, $useValidation = true) - { - $contents = file_get_contents($filePath); - $contents = $this->toContent($contents, $newValues, $useValidation); - file_put_contents($filePath, $contents); - return $contents; - } - - public function toContent($contents, $newValues, $useValidation = true) - { - $contents = $this->parseContent($contents, $newValues); - - if (!$useValidation) { - return $contents; - } - - $result = eval('?>'.$contents); - - foreach ($newValues as $key => $expectedValue) { - $parts = explode('.', $key); - - $array = $result; - foreach ($parts as $part) { - if (!is_array($array) || !array_key_exists($part, $array)) { - throw new Exception(sprintf('Unable to rewrite key "%s" in config, does it exist?', $key)); - } - - $array = $array[$part]; - } - $actualValue = $array; - - if ($actualValue != $expectedValue) { - throw new Exception(sprintf('Unable to rewrite key "%s" in config, rewrite failed', $key)); - } - } - - return $contents; - } - - protected function parseContent($contents, $newValues) - { - $result = $contents; - - foreach ($newValues as $path => $value) { - $result = $this->parseContentValue($result, $path, $value); - } - - return $result; - } - - protected function parseContentValue($contents, $path, $value) - { - $result = $contents; - $items = explode('.', $path); - $key = array_pop($items); - $replaceValue = $this->writeValueToPhp($value); - - $count = 0; - $patterns = []; - $patterns[] = $this->buildStringExpression($key, $items); - $patterns[] = $this->buildStringExpression($key, $items, '"'); - $patterns[] = $this->buildConstantExpression($key, $items); - $patterns[] = $this->buildArrayExpression($key, $items); - - foreach ($patterns as $pattern) { - $result = preg_replace($pattern, '${1}${2}'.$replaceValue, $result, 1, $count); - - if ($count > 0) { - break; - } - } - - return $result; - } - - protected function writeValueToPhp($value) - { - if (is_string($value) && strpos($value, "'") === false) { - $replaceValue = "'".$value."'"; - } - elseif (is_string($value) && strpos($value, '"') === false) { - $replaceValue = '"'.$value.'"'; - } - elseif (is_bool($value)) { - $replaceValue = ($value ? 'true' : 'false'); - } - elseif (is_null($value)) { - $replaceValue = 'null'; - } - elseif (is_array($value) && count($value) === count($value, COUNT_RECURSIVE)) { - $replaceValue = $this->writeArrayToPhp($value); - } - else { - $replaceValue = $value; - } - - $replaceValue = str_replace('$', '\$', $replaceValue); - - return $replaceValue; - } - - protected function writeArrayToPhp($array) + public function toFile(string $filePath, array $newValues): string { - $result = []; - - foreach ($array as $value) { - if (!is_array($value)) { - $result[] = $this->writeValueToPhp($value); - } - } - - return '['.implode(', ', $result).']'; + $arrayFile = ArrayFile::open($filePath)->set($newValues); + $arrayFile->write(); + return $arrayFile->render(); } - protected function buildStringExpression($targetKey, $arrayItems = [], $quoteChar = "'") + public function toContent(string $contents, $newValues): string { - $expression = []; - - // Opening expression for array items ($1) - $expression[] = $this->buildArrayOpeningExpression($arrayItems); - - // The target key opening - $expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)['.$quoteChar.']'; - - // The target value to be replaced ($2) - $expression[] = '([^'.$quoteChar.']*)'; - - // The target key closure - $expression[] = '['.$quoteChar.']'; - - return '/' . implode('', $expression) . '/'; - } + $lexer = new Emulative([ + 'usedAttributes' => [ + 'comments', + 'startTokenPos', + 'startLine', + 'endTokenPos', + 'endLine' + ] + ]); + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer); - /** - * Common constants only (true, false, null, integers) - */ - protected function buildConstantExpression($targetKey, $arrayItems = []) - { - $expression = []; - - // Opening expression for array items ($1) - $expression[] = $this->buildArrayOpeningExpression($arrayItems); - - // The target key opening ($2) - $expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)'; - - // The target value to be replaced ($3) - $expression[] = '([tT][rR][uU][eE]|[fF][aA][lL][sS][eE]|[nN][uU][lL]{2}|[\d]+)'; - - return '/' . implode('', $expression) . '/'; - } - - /** - * Single level arrays only - */ - protected function buildArrayExpression($targetKey, $arrayItems = []) - { - $expression = []; - - // Opening expression for array items ($1) - $expression[] = $this->buildArrayOpeningExpression($arrayItems); - - // The target key opening ($2) - $expression[] = '([\'|"]'.$targetKey.'[\'|"]\s*=>\s*)'; - - // The target value to be replaced ($3) - $expression[] = '(?:[aA][rR]{2}[aA][yY]\(|[\[])([^\]|)]*)[\]|)]'; - - return '/' . implode('', $expression) . '/'; - } - - protected function buildArrayOpeningExpression($arrayItems) - { - if (count($arrayItems)) { - $itemOpen = []; - foreach ($arrayItems as $item) { - // The left hand array assignment - $itemOpen[] = '[\'|"]'.$item.'[\'|"]\s*=>\s*(?:[aA][rR]{2}[aA][yY]\(|[\[])'; - } - - // Capture all opening array (non greedy) - $result = '(' . implode('[\s\S]*', $itemOpen) . '[\s\S]*?)'; - } - else { - // Gotta capture something for $1 - $result = '()'; + try { + $ast = $parser->parse($contents); + } catch (Error $e) { + throw new SystemException($e); } - return $result; + return (new ArrayFile($ast, $lexer, null))->set($newValues)->render(); } } diff --git a/src/Config/FileLoader.php b/src/Config/FileLoader.php index f1395c9cc..52c59f09e 100644 --- a/src/Config/FileLoader.php +++ b/src/Config/FileLoader.php @@ -210,14 +210,15 @@ protected function getPackagePath($package, $group, $env = null) /** * Get the configuration path for a namespace. * - * @param string $namespace + * @param string|null $namespace * @return string|null */ - protected function getPath($namespace) + protected function getPath($namespace = null) { if (is_null($namespace)) { return $this->defaultPath; - } elseif (isset($this->hints[$namespace])) { + } + if (isset($this->hints[$namespace])) { return $this->hints[$namespace]; } @@ -237,10 +238,10 @@ public function addNamespace($namespace, $hint) } /** - * Add a new namespace to the loader. + * Registers an alias for a given namespace. * - * @param string $namespace - * @param string $alias + * @param string $namespace + * @param string $alias * @return void */ public function registerNamespaceAlias(string $namespace, string $alias) diff --git a/src/Config/LoaderInterface.php b/src/Config/LoaderInterface.php index 7345546f3..7b5f4502b 100644 --- a/src/Config/LoaderInterface.php +++ b/src/Config/LoaderInterface.php @@ -31,6 +31,15 @@ public function exists($group, $namespace = null); */ public function addNamespace($namespace, $hint); + /** + * Registers an alias for a given namespace. + * + * @param string $namespace + * @param string $alias + * @return void + */ + public function registerNamespaceAlias(string $namespace, string $alias); + /** * Returns all registered namespaces with the config * loader. diff --git a/src/Config/Repository.php b/src/Config/Repository.php index 68caddbec..719802dfe 100644 --- a/src/Config/Repository.php +++ b/src/Config/Repository.php @@ -2,6 +2,7 @@ use Closure; use ArrayAccess; +use Illuminate\Config\Repository as BaseRepository; use Illuminate\Contracts\Config\Repository as RepositoryContract; /** @@ -9,7 +10,7 @@ * * @author Alexey Bobkov, Samuel Georges */ -class Repository implements ArrayAccess, RepositoryContract +class Repository extends BaseRepository implements ArrayAccess, RepositoryContract { use \Winter\Storm\Support\Traits\KeyParser; @@ -115,6 +116,27 @@ public function get($key, $default = null) return array_get($this->items[$collection], $item, $default); } + /** + * Get many configuration values. + * + * @param array $keys + * @return array + */ + public function getMany($keys) + { + $config = []; + + foreach ($keys as $key => $default) { + if (is_numeric($key)) { + [$key, $default] = [$default, null]; + } + + $config[$key] = $this->get($key, $default); + } + + return $config; + } + /** * Set a given configuration value. * @@ -148,48 +170,6 @@ public function set($key, $value = null) } } - /** - * Prepend a value onto an array configuration value. - * - * @param string $key - * @param mixed $value - * @return void - */ - public function prepend($key, $value) - { - $array = $this->get($key); - - array_unshift($array, $value); - - $this->set($key, $array); - } - - /** - * Push a value onto an array configuration value. - * - * @param string $key - * @param mixed $value - * @return void - */ - public function push($key, $value) - { - $array = $this->get($key); - - $array[] = $value; - - $this->set($key, $array); - } - - /** - * Get all of the configuration items for the application. - * - * @return array - */ - public function all() - { - return $this->items; - } - /** * Load the configuration group for the key. * @@ -462,7 +442,7 @@ public function getItems() * @param string $key * @return bool */ - public function offsetExists($key) + public function offsetExists($key): bool { return $this->has($key); } @@ -473,7 +453,7 @@ public function offsetExists($key) * @param string $key * @return mixed */ - public function offsetGet($key) + public function offsetGet($key): mixed { return $this->get($key); } @@ -485,7 +465,7 @@ public function offsetGet($key) * @param mixed $value * @return void */ - public function offsetSet($key, $value) + public function offsetSet($key, $value): void { $this->set($key, $value); } @@ -496,7 +476,7 @@ public function offsetSet($key, $value) * @param string $key * @return void */ - public function offsetUnset($key) + public function offsetUnset($key): void { $this->set($key, null); } diff --git a/src/Console/Command.php b/src/Console/Command.php new file mode 100644 index 000000000..b3a3c3af8 --- /dev/null +++ b/src/Console/Command.php @@ -0,0 +1,140 @@ +replaces)) { + $this->setAliases($this->replaces); + } + } + + /** + * Write a string in an alert box. + * + * @param string $string + * @return void + */ + public function alert($string) + { + $maxLength = 80; + $padding = 5; + $border = 1; + + // Wrap the string to the max length of the alert box + // taking into account the desired padding and border + $string = wordwrap($string, $maxLength - ($border * 2) - ($padding * 2)); + $lines = explode("\n", $string); + + // Identify the length of the longest line + $longest = 0; + foreach ($lines as $line) { + $length = strlen($line); + if ($length > $longest) { + $longest = $length; + } + } + $innerLineWidth = $longest + $padding; + $width = $innerLineWidth + ($border * 2); + + // Top border + $this->comment(str_repeat('*', $width)); + + // Alert content + foreach ($lines as $line) { + // Apply padding and borders to each line + $this->comment( + str_repeat('*', $border) + . str_pad($line, $innerLineWidth, ' ', STR_PAD_BOTH) + . str_repeat('*', $border) + ); + } + + // Bottom border + $this->comment(str_repeat('*', $width)); + + $this->newLine(); + } + + /** + * Provide autocompletion for this command's input + */ + public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void + { + $inputs = [ + 'arguments' => $input->getArguments(), + 'options' => $input->getOptions(), + ]; + + foreach ($inputs as $type => $data) { + switch ($type) { + case 'arguments': + $dataType = 'Argument'; + $suggestionType = 'Values'; + break; + case 'options': + $dataType = 'Option'; + $suggestionType = 'Options'; + break; + default: + // This should not be possible to ever be triggered given the type is hardcoded above + throw new \Exception('Invalid input type being parsed during completion'); + } + if (!empty($data)) { + foreach ($data as $name => $value) { + // Skip the command argument since that's handled by Artisan directly + if ( + $type === 'arguments' + && in_array($name, ['command']) + ) { + continue; + } + + $inputRoutingMethod = "mustSuggest{$dataType}ValuesFor"; + $suggestionValuesMethod = Str::camel('suggest ' . $name) . $suggestionType; + $suggestionsMethod = 'suggest' . $suggestionType; + + if ( + method_exists($this, $suggestionValuesMethod) + && $input->{$inputRoutingMethod}($name) + ) { + $values = $this->$suggestionValuesMethod($value, $inputs); + $suggestions->{$suggestionsMethod}($values); + } + } + } + } + } + + /** + * Example implementation of a suggestion method + */ + // public function suggestMyArgumentValues(string $value = null, array $allInput): array + // { + // if ($allInput['arguments']['dependent'] === 'matches') { + // return ['some', 'suggested', 'values']; + // } + // return ['all', 'values']; + // } +} diff --git a/src/Console/Traits/ConfirmsWithInput.php b/src/Console/Traits/ConfirmsWithInput.php new file mode 100644 index 000000000..8cc25d3e1 --- /dev/null +++ b/src/Console/Traits/ConfirmsWithInput.php @@ -0,0 +1,41 @@ +laravel->isProduction() && !$this->option('force')) { + $this->error("THE APPLICATION IS IN PRODUCTION"); + } + + $this->alert($message); + + $confirmed = false; + + if ($this->option('force')) { + $this->warn("The --force option was provided, proceeding without confirmation..."); + $confirmed = true; + } else { + $prompt = "Please type \"$requiredInput\" to proceed or CANCEL to cancel"; + do { + $input = $this->ask($prompt); + if (strtolower($input) === 'cancel') { + $confirmed = false; + break; + } + if (strtolower($input) === strtolower($requiredInput)) { + $confirmed = true; + } + } while ($confirmed === false); + } + + return $confirmed; + } +} diff --git a/src/Console/Traits/ProcessesQuery.php b/src/Console/Traits/ProcessesQuery.php new file mode 100644 index 000000000..19d88d758 --- /dev/null +++ b/src/Console/Traits/ProcessesQuery.php @@ -0,0 +1,74 @@ +count(); + + if (!$totalRecords) { + $this->warn("No records were found to process."); + return; + } + + $progress = $this->output->createProgressBar($totalRecords); + $progress->setFormat('%current%/%max% [%bar%] %percent:3s%% (%elapsed:6s%/%estimated:-6s%)'); + + $recordsProcessed = 0; + $limitReached = false; + + $query->chunkById($chunkSize, function ($records) use ($callback, $progress, &$recordsProcessed, $limit, &$limitReached) { + foreach ($records as $record) { + // Handle the limit being reached + if ($limit && $recordsProcessed >= $limit) { + $progress->finish(); + $this->info(''); + $this->error("Limit reached, " . number_format($recordsProcessed) . " records were processed."); + $limitReached = true; + return false; + } + + try { + // Process the record + $callback($record); + } catch (\Throwable $e) { + $recordsProcessed--; + $this->error(sprintf( + "Failed to process ID %s: %s", + $record->getKey(), + $e->getMessage() + )); + } + + // Attempt to avoid out of memory issues + unset($record); + + // Update the UI + $recordsProcessed++; + $progress->advance(); + } + }); + + if (!$limitReached) { + $progress->finish(); + $this->info(''); + } + + $this->info("Processed " . number_format($recordsProcessed) . " of " . number_format($totalRecords) . " records."); + $this->info(''); + } +} diff --git a/src/Cookie/CookieValuePrefix.php b/src/Cookie/CookieValuePrefix.php deleted file mode 100644 index b2d106790..000000000 --- a/src/Cookie/CookieValuePrefix.php +++ /dev/null @@ -1,47 +0,0 @@ -disableFor($except); } - - /** - * Shift gracefully to unserialized cookies - * @todo Remove entire method if year >= 2021 or build >= 475 - */ - protected function decryptCookie($name, $cookie) - { - if (is_array($cookie)) { - return $this->decryptArray($cookie); - } - - try { - $result = $this->encrypter->decrypt($cookie, true); - if (!is_string($result)) { - $result = json_encode($result); - } - } - catch (\Exception $ex) { - $result = $this->encrypter->decrypt($cookie, false); - } - - return $result; - } - - /** - * Shift gracefully to unserialized cookies - * @todo Remove entire method if year >= 2021 or build >= 475 - */ - protected function decryptArray(array $cookie) - { - $decrypted = []; - - foreach ($cookie as $key => $value) { - if (is_string($value)) { - try { - $result = $this->encrypter->decrypt($value, true); - if (!is_string($result)) { - $result = json_encode($result); - } - $decrypted[$key] = $result; - } - catch (\Exception $ex) { - $decrypted[$key] = $this->encrypter->decrypt($value, false); - } - } - } - - return $decrypted; - } - - /** - * Decrypt the cookies on the request. - * - * @param \Symfony\Component\HttpFoundation\Request $request - * @return \Symfony\Component\HttpFoundation\Request - */ - protected function decrypt(Request $request) - { - foreach ($request->cookies as $key => $cookie) { - if ($this->isDisabled($key)) { - continue; - } - - try { - // Decrypt the request-provided cookie - $decryptedValue = $this->decryptCookie($key, $cookie); - - // Verify that the decrypted value belongs to this cookie key, use null if it fails - $value = CookieValuePrefix::getVerifiedValue($key, $decryptedValue, $this->encrypter->getKey()); - - /** - * If the cookie is for the session and the value is a valid Session ID, - * then allow it to pass through even if the validation failed (most likely - * because the upgrade just occurred) - * - * The cookie will be adjusted on the next request - * @todo Remove if year >= 2021 or build >= 475 - */ - if (empty($value) && $key === Config::get('session.cookie') && Session::isValidId($decryptedValue)) { - $value = $decryptedValue; - } - - // Set the verified cookie value on the request - $request->cookies->set($key, $value); - } catch (DecryptException $e) { - $request->cookies->set($key, null); - } - } - - return $request; - } - - /** - * Encrypt the cookies on an outgoing response. - * - * @param \Symfony\Component\HttpFoundation\Response $response - * @return \Symfony\Component\HttpFoundation\Response - */ - protected function encrypt(Response $response) - { - foreach ($response->headers->getCookies() as $cookie) { - if ($this->isDisabled($cookie->getName())) { - continue; - } - - $response->headers->setCookie($this->duplicate( - $cookie, - $this->encrypter->encrypt( - // Prefix the cookie value to verify that it belongs to the current cookie - CookieValuePrefix::create($cookie->getName(), $this->encrypter->getKey()) . $cookie->getValue(), - static::serialized($cookie->getName()) - ) - )); - } - - return $response; - } } diff --git a/src/Database/Attach/BrokenImage.php b/src/Database/Attach/BrokenImage.php index 7da30e53e..54cd08d50 100644 --- a/src/Database/Attach/BrokenImage.php +++ b/src/Database/Attach/BrokenImage.php @@ -1,6 +1,6 @@ [], ]; /** - * @var array The attributes that are mass assignable. + * @var string[] The attributes that are mass assignable. */ protected $fillable = [ 'file_name', @@ -47,17 +53,17 @@ class File extends Model ]; /** - * @var array The attributes that aren't mass assignable. + * @var string[] The attributes that aren't mass assignable. */ protected $guarded = []; /** - * @var array Known image extensions. + * @var string[] Known image extensions. */ public static $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp']; /** - * @var array Hidden fields from array/json access + * @var array Hidden fields from array/json access */ protected $hidden = ['attachment_type', 'attachment_id', 'is_public']; @@ -93,16 +99,14 @@ class File extends Model /** * Creates a file object from a file an uploaded file. - * @param Symfony\Component\HttpFoundation\File\UploadedFile $uploadedFile + * + * @param UploadedFile $uploadedFile The uploaded file. + * @return static */ public function fromPost($uploadedFile) { - if ($uploadedFile === null) { - return; - } - $this->file_name = $uploadedFile->getClientOriginalName(); - $this->file_size = $uploadedFile->getClientSize(); + $this->file_size = $uploadedFile->getSize(); $this->content_type = $uploadedFile->getMimeType(); $this->disk_name = $this->getDiskName(); @@ -120,13 +124,12 @@ public function fromPost($uploadedFile) /** * Creates a file object from a file on the disk. + * + * @param string $filePath The path to the file. + * @return static */ public function fromFile($filePath, $filename = null) { - if ($filePath === null) { - return; - } - $file = new FileObj($filePath); $this->file_name = empty($filename) ? $file->getFilename() : $filename; $this->file_size = $file->getSize(); @@ -141,17 +144,12 @@ public function fromFile($filePath, $filename = null) /** * Creates a file object from raw data. * - * @param $data string Raw data - * @param $filename string Filename - * - * @return $this + * @param string $data The raw data. + * @param string $filename The name of the file. + * @return static */ public function fromData($data, $filename) { - if ($data === null) { - return; - } - $tempName = str_replace('.', '', uniqid('', true)) . '.tmp'; $tempPath = temp_path($tempName); FileHelper::put($tempPath, $data); @@ -164,9 +162,10 @@ public function fromData($data, $filename) /** * Creates a file object from url - * @param $url string URL - * @param $filename string Filename - * @return $this + * + * @param string $url The URL to retrieve and store. + * @param string|null $filename The name of the file. If null, the filename will be extracted from the URL. + * @return static */ public function fromUrl($url, $filename = null) { @@ -183,7 +182,8 @@ public function fromUrl($url, $filename = null) // Get the filename from the path $filename = pathinfo($filePath)['filename']; - // Attempt to detect the extension from the reported Content-Type, fall back to the original path extension if not able to guess + // Attempt to detect the extension from the reported Content-Type, fall back to the original path extension + // if not able to guess $mimesToExt = array_flip($this->autoMimeTypes); if (!empty($data->headers['Content-Type']) && isset($mimesToExt[$data->headers['Content-Type']])) { $ext = $mimesToExt[$data->headers['Content-Type']]; @@ -204,6 +204,7 @@ public function fromUrl($url, $filename = null) /** * Helper attribute for getPath. + * * @return string */ public function getPathAttribute() @@ -213,6 +214,7 @@ public function getPathAttribute() /** * Helper attribute for getExtension. + * * @return string */ public function getExtensionAttribute() @@ -222,6 +224,8 @@ public function getExtensionAttribute() /** * Used only when filling attributes. + * + * @param mixed $value * @return void */ public function setDataAttribute($value) @@ -231,7 +235,10 @@ public function setDataAttribute($value) /** * Helper attribute for get image width. - * @return string + * + * Returns `null` if this file is not an image. + * + * @return string|int|null */ public function getWidthAttribute() { @@ -240,11 +247,16 @@ public function getWidthAttribute() return $dimensions[0]; } + + return null; } /** * Helper attribute for get image height. - * @return string + * + * Returns `null` if this file is not an image. + * + * @return string|int|null */ public function getHeightAttribute() { @@ -253,10 +265,13 @@ public function getHeightAttribute() return $dimensions[1]; } + + return null; } /** * Helper attribute for file size in human format. + * * @return string */ public function getSizeAttribute() @@ -271,33 +286,34 @@ public function getSizeAttribute() /** * Outputs the raw file contents. * - * @param string $disposition The Content-Disposition to set, defaults to inline - * @param bool $returnResponse Defaults to false, returns a Response object instead of directly outputting to the browser - * @return Response | void + * @param string $disposition The Content-Disposition to set, defaults to `inline` + * @param bool $returnResponse Defaults to `false`, returns a Response object instead of directly outputting to the + * browser + * @return \Illuminate\Http\Response|void */ public function output($disposition = 'inline', $returnResponse = false) { $response = response($this->getContents())->withHeaders([ 'Content-type' => $this->getContentType(), 'Content-Disposition' => $disposition . '; filename="' . $this->file_name . '"', - 'Cache-Control' => 'private, no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0', + 'Cache-Control' => 'private, no-store, no-cache, must-revalidate, max-age=0', 'Accept-Ranges' => 'bytes', 'Content-Length' => $this->file_size, ]); if ($returnResponse) { return $response; - } else { - $response->sendHeaders(); - $response->sendContent(); } + + $response->sendHeaders(); + $response->sendContent(); } /** * Outputs the raw thumbfile contents. * - * @param integer $width - * @param integer $height + * @param int $width + * @param int $height * @param array $options [ * 'mode' => 'auto', * 'offset' => [0, 0], @@ -307,8 +323,9 @@ public function output($disposition = 'inline', $returnResponse = false) * 'extension' => 'auto', * 'disposition' => 'inline', * ] - * @param bool $returnResponse Defaults to false, returns a Response object instead of directly outputting to the browser - * @return Response | void + * @param bool $returnResponse Defaults to `false`, returns a Response object instead of directly outputting to the + * browser + * @return \Illuminate\Http\Response|void */ public function outputThumb($width, $height, $options = [], $returnResponse = false) { @@ -321,17 +338,17 @@ public function outputThumb($width, $height, $options = [], $returnResponse = fa $response = response($contents)->withHeaders([ 'Content-type' => $this->getContentType(), 'Content-Disposition' => $disposition . '; filename="' . basename($thumbFile) . '"', - 'Cache-Control' => 'private, no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0', + 'Cache-Control' => 'private, no-store, no-cache, must-revalidate, max-age=0', 'Accept-Ranges' => 'bytes', 'Content-Length' => mb_strlen($contents, '8bit'), ]); if ($returnResponse) { return $response; - } else { - $response->sendHeaders(); - $response->sendContent(); } + + $response->sendHeaders(); + $response->sendContent(); } // @@ -341,7 +358,7 @@ public function outputThumb($width, $height, $options = [], $returnResponse = fa /** * Returns the cache key used for the hasFile method * - * @param string $path The path to get the cache key for + * @param string|null $path The path to get the cache key for * @return string */ public function getCacheKey($path = null) @@ -355,6 +372,8 @@ public function getCacheKey($path = null) /** * Returns the file name without path + * + * @return string */ public function getFilename() { @@ -363,6 +382,8 @@ public function getFilename() /** * Returns the file extension. + * + * @return string */ public function getExtension() { @@ -371,6 +392,8 @@ public function getExtension() /** * Returns the last modification date as a UNIX timestamp. + * + * @param string|null $fileName * @return int */ public function getLastModified($fileName = null) @@ -380,6 +403,10 @@ public function getLastModified($fileName = null) /** * Returns the file content type. + * + * Returns `null` if the file content type cannot be determined. + * + * @return string|null */ public function getContentType() { @@ -397,6 +424,9 @@ public function getContentType() /** * Get file contents from storage device. + * + * @param string|null $fileName + * @return string */ public function getContents($fileName = null) { @@ -405,6 +435,9 @@ public function getContents($fileName = null) /** * Returns the public address to access the file. + * + * @param string|null $fileName + * @return string */ public function getPath($fileName = null) { @@ -417,6 +450,8 @@ public function getPath($fileName = null) /** * Returns a local path to this file. If the file is stored remotely, * it will be downloaded to a temporary directory. + * + * @return string */ public function getLocalPath() { @@ -437,6 +472,8 @@ public function getLocalPath() /** * Returns the path to the file, relative to the storage disk. + * + * @param string|null $fileName * @return string */ public function getDiskPath($fileName = null) @@ -449,15 +486,17 @@ public function getDiskPath($fileName = null) /** * Determines if the file is flagged "public" or not. + * + * @return bool */ public function isPublic() { if (array_key_exists('is_public', $this->attributes)) { - return $this->attributes['is_public']; + return (bool) $this->attributes['is_public']; } if (isset($this->is_public)) { - return $this->is_public; + return (bool) $this->is_public; } return true; @@ -465,7 +504,8 @@ public function isPublic() /** * Returns the file size as string. - * @return string Returns the size as string. + * + * @return string */ public function sizeToString() { @@ -479,6 +519,8 @@ public function sizeToString() /** * Before the model is saved * - check if new file data has been supplied, eg: $model->data = Input::file('something'); + * + * @return void */ public function beforeSave() { @@ -488,8 +530,7 @@ public function beforeSave() if ($this->data !== null) { if ($this->data instanceof UploadedFile) { $this->fromPost($this->data); - } - else { + } else { $this->fromFile($this->data); } @@ -500,14 +541,15 @@ public function beforeSave() /** * After model is deleted * - clean up it's thumbnails + * + * @return void */ public function afterDelete() { try { $this->deleteThumbs(); $this->deleteFile(); - } - catch (Exception $ex) { + } catch (Exception $ex) { } } @@ -517,6 +559,8 @@ public function afterDelete() /** * Checks if the file extension is an image and returns true or false. + * + * @return bool */ public function isImage() { @@ -525,7 +569,8 @@ public function isImage() /** * Get image dimensions - * @return array|bool + * + * @return array|false */ protected function getImageDimensions() { @@ -565,8 +610,7 @@ public function getThumb($width, $height, $options = []) if (!$this->hasFile($thumbFile)) { if ($this->isLocalStorage()) { $this->makeThumbLocal($thumbFile, $thumbPath, $width, $height, $options); - } - else { + } else { $this->makeThumbStorage($thumbFile, $thumbPath, $width, $height, $options); } } @@ -576,16 +620,37 @@ public function getThumb($width, $height, $options = []) /** * Generates a thumbnail filename. - * @return string + * + * @param integer $width + * @param integer $height + * @param array $options [ + * 'mode' => 'auto', + * 'offset' => [0, 0], + * 'quality' => 90, + * 'sharpen' => 0, + * 'interlace' => false, + * 'extension' => 'auto', + * ] + * @return string The filename of the thumbnail */ - public function getThumbFilename($width, $height, $options) + public function getThumbFilename($width, $height, $options = []) { $options = $this->getDefaultThumbOptions($options); - return 'thumb_' . $this->id . '_' . $width . '_' . $height . '_' . $options['offset'][0] . '_' . $options['offset'][1] . '_' . $options['mode'] . '.' . $options['extension']; + return implode('_', [ + 'thumb', + (string) $this->id, + (string) $width, + (string) $height, + (string) $options['offset'][0], + (string) $options['offset'][1], + (string) $options['mode'] . '.' . (string) $options['extension'], + ]); } /** * Returns the default thumbnail options. + * + * @param array $overrideOptions Overridden options * @return array */ protected function getDefaultThumbOptions($overrideOptions = []) @@ -615,9 +680,17 @@ protected function getDefaultThumbOptions($overrideOptions = []) } /** - * Generate the thumbnail based on the local file system. This step is necessary - * to simplify things and ensure the correct file permissions are given + * Generate the thumbnail based on the local file system. + * + * This step is necessary to simplify things and ensure the correct file permissions are given * to the local files. + * + * @param string $thumbFile + * @param string $thumbPath + * @param int $width + * @param int $height + * @param array $options + * @return void */ protected function makeThumbLocal($thumbFile, $thumbPath, $width, $height, $options) { @@ -630,18 +703,16 @@ protected function makeThumbLocal($thumbFile, $thumbPath, $width, $height, $opti */ if (!$this->hasFile($this->disk_name)) { BrokenImage::copyTo($thumbPath); - } - /* - * Generate thumbnail - */ - else { + } else { + /* + * Generate thumbnail + */ try { Resizer::open($filePath) ->resize($width, $height, $options) ->save($thumbPath) ; - } - catch (Exception $ex) { + } catch (Exception $ex) { Log::error($ex); BrokenImage::copyTo($thumbPath); } @@ -652,6 +723,13 @@ protected function makeThumbLocal($thumbFile, $thumbPath, $width, $height, $opti /** * Generate the thumbnail based on a remote storage engine. + * + * @param string $thumbFile + * @param string $thumbPath + * @param int $width + * @param int $height + * @param array $options + * @return void */ protected function makeThumbStorage($thumbFile, $thumbPath, $width, $height, $options) { @@ -663,11 +741,10 @@ protected function makeThumbStorage($thumbFile, $thumbPath, $width, $height, $op */ if (!$this->hasFile($this->disk_name)) { BrokenImage::copyTo($tempThumb); - } - /* - * Generate thumbnail - */ - else { + } else { + /* + * Generate thumbnail + */ $this->copyStorageToLocal($this->getDiskPath(), $tempFile); try { @@ -675,8 +752,7 @@ protected function makeThumbStorage($thumbFile, $thumbPath, $width, $height, $op ->resize($width, $height, $options) ->save($tempThumb) ; - } - catch (Exception $ex) { + } catch (Exception $ex) { Log::error($ex); BrokenImage::copyTo($tempThumb); } @@ -691,8 +767,10 @@ protected function makeThumbStorage($thumbFile, $thumbPath, $width, $height, $op FileHelper::delete($tempThumb); } - /* + /** * Delete all thumbnails for this file. + * + * @return void */ public function deleteThumbs() { @@ -713,8 +791,7 @@ public function deleteThumbs() if (!empty($collection)) { if ($this->isLocalStorage()) { FileHelper::delete($collection); - } - else { + } else { $this->getDisk()->delete($collection); } } @@ -726,6 +803,8 @@ public function deleteThumbs() /** * Generates a disk name from the supplied file name. + * + * @return string */ protected function getDiskName() { @@ -740,13 +819,16 @@ protected function getDiskName() $ext = $this->data->guessExtension(); } - $name = str_replace('.', '', uniqid(null, true)); + $name = str_replace('.', '', uniqid('', true)); return $this->disk_name = !empty($ext) ? $name.'.'.$ext : $name; } /** * Returns a temporary local path to work from. + * + * @param string|null $path Optional path to append to the temp path + * @return string */ protected function getLocalTempPath($path = null) { @@ -759,8 +841,10 @@ protected function getLocalTempPath($path = null) /** * Saves a file + * * @param string $sourcePath An absolute local path to a file name to read from. - * @param string $destinationFileName A storage file name to save to. + * @param string|null $destinationFileName A storage file name to save to. + * @return bool */ protected function putFile($sourcePath, $destinationFileName = null) { @@ -787,10 +871,9 @@ protected function putFile($sourcePath, $destinationFileName = null) */ if ( !FileHelper::isDirectory($destinationPath) && - !FileHelper::makeDirectory($destinationPath, 0777, true, true) && - !FileHelper::isDirectory($destinationPath) + !FileHelper::makeDirectory($destinationPath, 0777, true, true) ) { - trigger_error(error_get_last(), E_USER_WARNING); + trigger_error(error_get_last()['message'], E_USER_WARNING); } return FileHelper::copy($sourcePath, $destinationPath . $destinationFileName); @@ -798,6 +881,8 @@ protected function putFile($sourcePath, $destinationFileName = null) /** * Delete file contents from storage device. + * + * @param string|null $fileName * @return void */ protected function deleteFile($fileName = null) @@ -819,7 +904,9 @@ protected function deleteFile($fileName = null) /** * Check file exists on storage device. - * @return void + * + * @param string|null $fileName + * @return bool */ protected function hasFile($fileName = null) { @@ -838,8 +925,9 @@ protected function hasFile($fileName = null) } /** - * Checks if directory is empty then deletes it, - * three levels up to match the partition directory. + * Checks if directory is empty then deletes it, three levels up to match the partition directory. + * + * @param string|null $dir Directory to check and delete if empty. * @return void */ protected function deleteEmptyDirectory($dir = null) @@ -867,14 +955,12 @@ protected function deleteEmptyDirectory($dir = null) /** * Returns true if a directory contains no files. - * @return void + * + * @param string|null $dir Directory to check. + * @return bool */ - protected function isDirectoryEmpty($dir) + protected function isDirectoryEmpty($dir = null) { - if (!$dir) { - return null; - } - return count($this->storageCmd('allFiles', $dir)) === 0; } @@ -884,10 +970,10 @@ protected function isDirectoryEmpty($dir) /** * Calls a method against File or Storage depending on local storage. - * This allows local storage outside the storage/app folder and is - * also good for performance. For local storage, *every* argument - * is prefixed with the local root path. Props to Laravel for - * the unified interface. + * + * This allows local storage outside the storage/app folder and is also good for performance. For local storage, + * *every* argument is prefixed with the local root path. Props to Laravel for the unified interface. + * * @return mixed */ protected function storageCmd() @@ -904,8 +990,7 @@ protected function storageCmd() }, $args); $result = forward_static_call_array([$interface, $command], $args); - } - else { + } else { $result = call_user_func_array([$this->getDisk(), $command], $args); } @@ -914,6 +999,10 @@ protected function storageCmd() /** * Copy the Storage to local file + * + * @param string $storagePath + * @param string $localPath + * @return int The filesize of the copied file. */ protected function copyStorageToLocal($storagePath, $localPath) { @@ -922,6 +1011,10 @@ protected function copyStorageToLocal($storagePath, $localPath) /** * Copy the local file to Storage + * + * @param string $storagePath + * @param string $localPath + * @return string|bool */ protected function copyLocalToStorage($localPath, $storagePath) { @@ -933,8 +1026,9 @@ protected function copyLocalToStorage($localPath, $storagePath) // /** - * Returns the maximum size of an uploaded file as configured in php.ini - * @return int The maximum size of an uploaded file in kilobytes + * Returns the maximum size of an uploaded file as configured in php.ini in kilobytes (rounded) + * + * @return float */ public static function getMaxFilesize() { @@ -943,6 +1037,8 @@ public static function getMaxFilesize() /** * Define the internal storage path, override this method to define. + * + * @return string */ public function getStorageDirectory() { @@ -955,6 +1051,8 @@ public function getStorageDirectory() /** * Define the public address for the storage path. + * + * @return string */ public function getPublicPath() { @@ -967,6 +1065,8 @@ public function getPublicPath() /** * Define the internal working path, override this method to define. + * + * @return string */ public function getTempPath() { @@ -981,7 +1081,8 @@ public function getTempPath() /** * Returns the storage disk the file is stored on - * @return FilesystemAdapter + * + * @return Filesystem */ public function getDisk() { @@ -990,6 +1091,7 @@ public function getDisk() /** * Returns true if the storage engine is local. + * * @return bool */ protected function isLocalStorage() @@ -998,12 +1100,12 @@ protected function isLocalStorage() } /** - * Generates a partition for the file. - * return /ABC/DE1/234 for an name of ABCDE1234. - * @param Attachment $attachment - * @param string $styleName - * @return mixed - */ + * Generates a partition for the file. + * + * For example, returns `/ABC/DE1/234` for an name of `ABCDE1234`. + * + * @return string + */ protected function getPartitionDirectory() { return implode('/', array_slice(str_split($this->disk_name, 3), 0, 3)) . '/'; @@ -1011,10 +1113,11 @@ protected function getPartitionDirectory() /** * If working with local storage, determine the absolute local path. + * * @return string */ protected function getLocalRootPath() { - return storage_path().'/app'; + return storage_path() . '/app'; } } diff --git a/src/Database/Attach/Resizer.php b/src/Database/Attach/Resizer.php index 777d4b972..f0f199496 100644 --- a/src/Database/Attach/Resizer.php +++ b/src/Database/Attach/Resizer.php @@ -1,7 +1,8 @@ retainImageTransparency($img); break; default: - throw new Exception(sprintf('Invalid mime type: %s. Accepted types: image/jpeg, image/gif, image/png, image/webp.', $this->mime)); - break; + throw new Exception( + sprintf( + 'Invalid mime type: %s. Accepted types: image/jpeg, image/gif, image/png, image/webp.', + $this->mime + ) + ); } return $img; diff --git a/src/Database/Behaviors/Purgeable.php b/src/Database/Behaviors/Purgeable.php index cd37d787c..dd1c84d68 100644 --- a/src/Database/Behaviors/Purgeable.php +++ b/src/Database/Behaviors/Purgeable.php @@ -3,11 +3,10 @@ class Purgeable extends \Winter\Storm\Extension\ExtensionBase { /** - * @var array List of attribute names which should not be saved to the database. + * Model to purge. * - * public $purgeable = []; + * @var \Winter\Storm\Database\Model */ - protected $model; public function __construct($parent) @@ -47,7 +46,7 @@ public function bootPurgeable() /** * Adds an attribute to the purgeable attributes list * @param array|string|null $attributes - * @return $this + * @return \Winter\Storm\Database\Model */ public function addPurgeable($attributes = null) { @@ -60,7 +59,7 @@ public function addPurgeable($attributes = null) /** * Removes purged attributes from the dataset, used before saving. - * @param $attributes mixed Attribute(s) to purge, if unspecified, $purgable property is used + * @param string|array|null $attributesToPurge Attribute(s) to purge. If unspecified, $purgable property is used * @return array Current attribute set */ public function purgeAttributes($attributesToPurge = null) @@ -76,12 +75,7 @@ public function purgeAttributes($attributesToPurge = null) $cleanAttributes = array_diff_key($attributes, array_flip($purgeable)); $originalAttributes = array_diff_key($attributes, $cleanAttributes); - if (is_array($this->originalPurgeableValues)) { - $this->originalPurgeableValues = array_merge($this->originalPurgeableValues, $originalAttributes); - } - else { - $this->originalPurgeableValues = $originalAttributes; - } + $this->originalPurgeableValues = array_merge($this->originalPurgeableValues, $originalAttributes); return $this->model->attributes = $cleanAttributes; } @@ -112,6 +106,8 @@ public function getOriginalPurgeValue($attribute) /** * Restores the original values of any purged attributes. + * + * @return \Winter\Storm\Database\Model */ public function restorePurgedValues() { diff --git a/src/Database/Builder.php b/src/Database/Builder.php index c39264053..2d760194d 100644 --- a/src/Database/Builder.php +++ b/src/Database/Builder.php @@ -10,9 +10,24 @@ * Extends Eloquent builder class. * * @author Alexey Bobkov, Samuel Georges + * @mixin \Winter\Storm\Database\QueryBuilder */ class Builder extends BuilderModel { + /** + * The base query builder instance. + * + * @var \Winter\Storm\Database\QueryBuilder + */ + protected $query; + + /** + * The model being queried. + * + * @var \Winter\Storm\Database\Model + */ + protected $model; + /** * Get an array with the values of a given column. * @@ -103,10 +118,14 @@ protected function searchWhereInternal($term, $columns, $mode, $boolean) /** * Paginate the given query. * - * @param int $perPage - * @param int $currentPage - * @param array $columns - * @param string $pageName + * This method also accepts the Laravel signature: + * + * `paginate(int|null $perPage, array $columns, string $pageName, int|null $page)` + * + * @param int|null $perPage + * @param array|int|null $currentPage + * @param array|string $columns + * @param string|int|null $pageName * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator */ public function paginate($perPage = null, $currentPage = null, $columns = ['*'], $pageName = 'page') @@ -146,9 +165,14 @@ public function paginate($perPage = null, $currentPage = null, $columns = ['*'], /** * Paginate the given query into a simple paginator. * - * @param int $perPage - * @param int $currentPage - * @param array $columns + * This method also accepts the Laravel signature: + * + * `simplePaginate(int|null $perPage, array $columns, string $pageName, int|null $page)` + * + * @param int|null $perPage + * @param array|int|null $currentPage + * @param array|string $columns + * @param string|int|null $pageName * @return \Illuminate\Contracts\Pagination\Paginator */ public function simplePaginate($perPage = null, $currentPage = null, $columns = ['*'], $pageName = 'page') diff --git a/src/Database/Concerns/HasRelationships.php b/src/Database/Concerns/HasRelationships.php index 9ce32d4ad..ecf581b9c 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -134,7 +134,6 @@ trait HasRelationships 'hasManyThrough' ]; - // // Relations // @@ -152,13 +151,15 @@ public function hasRelation($name) /** * Returns relationship details from a supplied name. * @param string $name Relation name - * @return array + * @return array|null */ public function getRelationDefinition($name) { if (($type = $this->getRelationType($name)) !== null) { return (array) $this->getRelationTypeDefinition($type, $name) + $this->getRelationDefaults($type); } + + return null; } /** @@ -179,7 +180,7 @@ public function getRelationTypeDefinitions($type) * Returns the given relation definition. * @param string $type Relation type * @param string $name Relation name - * @return array + * @return string|null */ public function getRelationTypeDefinition($type, $name) { @@ -188,6 +189,8 @@ public function getRelationTypeDefinition($type, $name) if (isset($definitions[$name])) { return $definitions[$name]; } + + return null; } /** @@ -217,7 +220,7 @@ public function getRelationDefinitions() /** * Returns a relationship type based on a supplied name. * @param string $name Relation name - * @return string + * @return string|null */ public function getRelationType($name) { @@ -226,12 +229,14 @@ public function getRelationType($name) return $type; } } + + return null; } /** * Returns a relation class object * @param string $name Relation name - * @return string + * @return \Winter\Storm\Database\Relations\Relation|null */ public function makeRelation($name) { @@ -337,6 +342,11 @@ protected function handleRelation($relationName) case 'morphToMany': $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']); $relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], false, $relationName); + + if (isset($relation['pivotModel'])) { + $relationObj->using($relation['pivotModel']); + } + break; case 'morphedByMany': @@ -466,7 +476,7 @@ public function belongsTo($related, $foreignKey = null, $parentKey = null, $rela /** * Define an polymorphic, inverse one-to-one or many relationship. * Overridden from {@link Eloquent\Model} to allow the usage of the intermediary methods to handle the relation. - * @return \Winter\Storm\Database\Relations\BelongsTo + * @return \Winter\Storm\Database\Relations\MorphTo */ public function morphTo($name = null, $type = null, $id = null, $ownerKey = null) { @@ -488,7 +498,7 @@ public function morphTo($name = null, $type = null, $id = null, $ownerKey = null * @param string $type * @param string $id * @param string $ownerKey - * @return \Illuminate\Database\Eloquent\Relations\MorphTo + * @return \Winter\Storm\Database\Relations\MorphTo */ protected function morphEagerTo($name, $type, $id, $ownerKey) { @@ -509,10 +519,10 @@ protected function morphEagerTo($name, $type, $id, $ownerKey) * @param string $name * @param string $type * @param string $id - * @param string $ownerKey - * @return \Illuminate\Database\Eloquent\Relations\MorphTo + * @param string|null $ownerKey + * @return \Winter\Storm\Database\Relations\MorphTo */ - protected function morphInstanceTo($target, $name, $type, $id, $ownerKey) + protected function morphInstanceTo($target, $name, $type, $id, $ownerKey = null) { $instance = $this->newRelatedInstance( static::getActualClassNameForMorph($target) @@ -719,7 +729,7 @@ public function morphedByMany($related, $name, $table = null, $primaryKey = null /** * Define an attachment one-to-one relationship. * This code is a duplicate of Eloquent but uses a Storm relation class. - * @return \Winter\Storm\Database\Relations\MorphOne + * @return \Winter\Storm\Database\Relations\AttachOne */ public function attachOne($related, $isPublic = true, $localKey = null, $relationName = null) { @@ -741,7 +751,7 @@ public function attachOne($related, $isPublic = true, $localKey = null, $relatio /** * Define an attachment one-to-many relationship. * This code is a duplicate of Eloquent but uses a Storm relation class. - * @return \Winter\Storm\Database\Relations\MorphMany + * @return \Winter\Storm\Database\Relations\AttachMany */ public function attachMany($related, $isPublic = null, $localKey = null, $relationName = null) { @@ -765,7 +775,7 @@ public function attachMany($related, $isPublic = null, $localKey = null, $relati */ protected function getRelationCaller() { - $backtrace = debug_backtrace(false); + $backtrace = debug_backtrace(0); $caller = ($backtrace[2]['function'] == 'handleRelation') ? $backtrace[4] : $backtrace[2]; return $caller['function']; } @@ -785,4 +795,178 @@ protected function setRelationValue($relationName, $value) { $this->$relationName()->setSimpleValue($value); } + + /** + * Dynamically add the provided relationship configuration to the local properties + * + * @throws InvalidArgumentException if the $type is invalid or if the $name is already in use + */ + protected function addRelation(string $type, string $name, array $config): void + { + if (!in_array($type, static::$relationTypes)) { + throw new InvalidArgumentException( + sprintf( + 'Cannot add the "%s" relation to %s, %s is not a valid relationship type.', + $name, + get_class($this), + $type + ) + ); + } + + if ($this->hasRelation($name) || isset($this->{$name})) { + throw new InvalidArgumentException( + sprintf( + 'Cannot add the "%s" relation to %s, it conflicts with an existing relation, attribute, or property.', + $name, + get_class($this) + ) + ); + } + + $this->{$type} = array_merge($this->{$type}, [$name => $config]); + } + + /** + * Dynamically add a HasOne relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addHasOneRelation(string $name, array $config): void + { + $this->addRelation('hasOne', $name, $config); + } + + /** + * Dynamically add a HasMany relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addHasManyRelation(string $name, array $config): void + { + $this->addRelation('hasMany', $name, $config); + } + + /** + * Dynamically add a BelongsTo relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addBelongsToRelation(string $name, array $config): void + { + $this->addRelation('belongsTo', $name, $config); + } + + /** + * Dynamically add a BelongsToMany relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addBelongsToManyRelation(string $name, array $config): void + { + $this->addRelation('belongsToMany', $name, $config); + } + + /** + * Dynamically add a MorphTo relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addMorphToRelation(string $name, array $config): void + { + $this->addRelation('morphTo', $name, $config); + } + + /** + * Dynamically add a MorphOne relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addMorphOneRelation(string $name, array $config): void + { + $this->addRelation('morphOne', $name, $config); + } + + /** + * Dynamically add a MorphMany relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addMorphManyRelation(string $name, array $config): void + { + $this->addRelation('morphMany', $name, $config); + } + + /** + * Dynamically add a MorphToMany relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addMorphToManyRelation(string $name, array $config): void + { + $this->addRelation('morphToMany', $name, $config); + } + + /** + * Dynamically add a MorphedByMany relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addMorphedByManyRelation(string $name, array $config): void + { + $this->addRelation('morphedByMany', $name, $config); + } + + /** + * Dynamically add an AttachOne relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addAttachOneRelation(string $name, array $config): void + { + $this->addRelation('attachOne', $name, $config); + } + + /** + * Dynamically add an AttachMany relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addAttachManyRelation(string $name, array $config): void + { + $this->addRelation('attachMany', $name, $config); + } + + /** + * Dynamically add a(n) HasOneThrough relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addHasOneThroughRelation(string $name, array $config): void + { + $this->addRelation('HasOneThrough', $name, $config); + } + + /** + * Dynamically add a(n) HasManyThrough relationship + * + * @throws InvalidArgumentException if the provided relationship is already defined + */ + public function addHasManyThroughRelation(string $name, array $config): void + { + $this->addRelation('HasManyThrough', $name, $config); + } + + /** + * Get the polymorphic relationship columns. + * + * @param string $name + * @param string|null $type + * @param string|null $id + * @return array + */ + protected function getMorphs($name, $type = null, $id = null) + { + return [$type ?: $name.'_type', $id ?: $name.'_id']; + } } diff --git a/src/Database/Concerns/HidesAttributes.php b/src/Database/Concerns/HidesAttributes.php new file mode 100644 index 000000000..2f38c27aa --- /dev/null +++ b/src/Database/Concerns/HidesAttributes.php @@ -0,0 +1,41 @@ +hidden = array_merge( + $this->hidden, + is_array($attributes) ? $attributes : func_get_args() + ); + } + + /** + * Add visible attributes for the model. + * + * This restores the `addVisible` method that was removed from Laravel 7 onwards. It is however recommended to use + * the `makeVisible` method going forward. + * + * @param array|string|null $attributes + */ + public function addVisible($attributes = null): void + { + $this->visible = array_merge( + $this->visible, + is_array($attributes) ? $attributes : func_get_args() + ); + } +} diff --git a/src/Database/Connections/Connection.php b/src/Database/Connections/Connection.php index dcd804cb7..062c6205c 100644 --- a/src/Database/Connections/Connection.php +++ b/src/Database/Connections/Connection.php @@ -39,9 +39,7 @@ public static function flushDuplicateCache() */ public function logQuery($query, $bindings, $time = null) { - if (isset($this->events)) { - $this->events->fire('illuminate.query', [$query, $bindings, $time, $this->getName()]); - } + $this->fireEvent('illuminate.query', [$query, $bindings, $time, $this->getName()]); parent::logQuery($query, $bindings, $time); } @@ -50,14 +48,27 @@ public function logQuery($query, $bindings, $time = null) * Fire an event for this connection. * * @param string $event - * @return void + * @return array|null */ protected function fireConnectionEvent($event) { - if (isset($this->events)) { - $this->events->fire('connection.'.$this->getName().'.'.$event, $this); - } + $this->fireEvent('connection.'.$this->getName().'.'.$event, $this); parent::fireConnectionEvent($event); } + + /** + * Fire the given event if possible. + */ + protected function fireEvent(string $event, array|object $attributes = []): void + { + /** @var \Winter\Storm\Events\Dispatcher|null */ + $eventManager = $this->events; + + if (!isset($eventManager)) { + return; + } + + $eventManager->fire($event, $attributes); + } } diff --git a/src/Database/Connections/MySqlConnection.php b/src/Database/Connections/MySqlConnection.php index 8082c7399..2458a9ee6 100644 --- a/src/Database/Connections/MySqlConnection.php +++ b/src/Database/Connections/MySqlConnection.php @@ -1,18 +1,21 @@ schemaGrammar)) { + if (!isset($this->schemaGrammar)) { $this->useDefaultSchemaGrammar(); } @@ -36,7 +39,7 @@ public function getSchemaBuilder() /** * Get the default schema grammar instance. * - * @return \Illuminate\Database\Schema\Grammars\MySqlGrammar + * @return \Illuminate\Database\Grammar */ protected function getDefaultSchemaGrammar() { @@ -56,11 +59,11 @@ protected function getDefaultPostProcessor() /** * Get the Doctrine DBAL driver. * - * @return \Doctrine\DBAL\Driver\PDOMySql\Driver + * @return \Illuminate\Database\PDO\MySqlDriver */ protected function getDoctrineDriver() { - return new DoctrineDriver; + return new MySqlDriver; } /** diff --git a/src/Database/Connections/PostgresConnection.php b/src/Database/Connections/PostgresConnection.php index e012a260b..d768d69e4 100644 --- a/src/Database/Connections/PostgresConnection.php +++ b/src/Database/Connections/PostgresConnection.php @@ -1,17 +1,20 @@ schemaGrammar)) { + if (!isset($this->schemaGrammar)) { $this->useDefaultSchemaGrammar(); } @@ -35,7 +38,7 @@ public function getSchemaBuilder() /** * Get the default schema grammar instance. * - * @return \Illuminate\Database\Schema\Grammars\PostgresGrammar + * @return \Illuminate\Database\Grammar */ protected function getDefaultSchemaGrammar() { @@ -55,10 +58,10 @@ protected function getDefaultPostProcessor() /** * Get the Doctrine DBAL driver. * - * @return \Doctrine\DBAL\Driver\PDOPgSql\Driver + * @return \Illuminate\Database\PDO\PostgresDriver */ protected function getDoctrineDriver() { - return new DoctrineDriver; + return new PostgresDriver; } } diff --git a/src/Database/Connections/SQLiteConnection.php b/src/Database/Connections/SQLiteConnection.php index eaf4e49e2..1d6e3db1e 100644 --- a/src/Database/Connections/SQLiteConnection.php +++ b/src/Database/Connections/SQLiteConnection.php @@ -2,16 +2,19 @@ use Illuminate\Database\Schema\SQLiteBuilder; use Illuminate\Database\Query\Processors\SQLiteProcessor; -use Doctrine\DBAL\Driver\PDOSqlite\Driver as DoctrineDriver; +use Illuminate\Database\PDO\SQLiteDriver; use Winter\Storm\Database\Query\Grammars\SQLiteGrammar as QueryGrammar; use Illuminate\Database\Schema\Grammars\SQLiteGrammar as SchemaGrammar; +/** + * @phpstan-property \Illuminate\Database\Schema\Grammars\Grammar|null $schemaGrammar + */ class SQLiteConnection extends Connection { /** * Get the default query grammar instance. * - * @return \Winter\Storm\Database\Query\Grammars\SQLiteGrammar + * @return \Illuminate\Database\Grammar */ protected function getDefaultQueryGrammar() { @@ -25,7 +28,7 @@ protected function getDefaultQueryGrammar() */ public function getSchemaBuilder() { - if (is_null($this->schemaGrammar)) { + if (!isset($this->schemaGrammar)) { $this->useDefaultSchemaGrammar(); } @@ -35,7 +38,7 @@ public function getSchemaBuilder() /** * Get the default schema grammar instance. * - * @return \Winter\Storm\Database\Query\Grammars\SQLiteGrammar + * @return \Illuminate\Database\Grammar */ protected function getDefaultSchemaGrammar() { @@ -55,10 +58,10 @@ protected function getDefaultPostProcessor() /** * Get the Doctrine DBAL driver. * - * @return \Doctrine\DBAL\Driver\PDOSqlite\Driver + * @return \Illuminate\Database\PDO\SQLiteDriver */ protected function getDoctrineDriver() { - return new DoctrineDriver; + return new SQLiteDriver; } } diff --git a/src/Database/Connections/SqlServerConnection.php b/src/Database/Connections/SqlServerConnection.php index e48c45167..07a6af21a 100644 --- a/src/Database/Connections/SqlServerConnection.php +++ b/src/Database/Connections/SqlServerConnection.php @@ -4,11 +4,14 @@ use Exception; use Throwable; use Illuminate\Database\Schema\SqlServerBuilder; -use Doctrine\DBAL\Driver\PDOSqlsrv\Driver as DoctrineDriver; +use Illuminate\Database\PDO\SqlServerDriver; use Illuminate\Database\Query\Processors\SqlServerProcessor; -use Winter\Storm\Database\Query\Grammars\SqlServerGrammar as QueryGrammar; use Illuminate\Database\Schema\Grammars\SqlServerGrammar as SchemaGrammar; +use Winter\Storm\Database\Query\Grammars\SqlServerGrammar as QueryGrammar; +/** + * @phpstan-property \Illuminate\Database\Schema\Grammars\Grammar|null $schemaGrammar + */ class SqlServerConnection extends Connection { /** @@ -57,7 +60,7 @@ public function transaction(Closure $callback, $attempts = 1) /** * Get the default query grammar instance. * - * @return \Illuminate\Database\Query\Grammars\SqlServerGrammar + * @return \Illuminate\Database\Grammar */ protected function getDefaultQueryGrammar() { @@ -71,7 +74,7 @@ protected function getDefaultQueryGrammar() */ public function getSchemaBuilder() { - if (is_null($this->schemaGrammar)) { + if (!isset($this->schemaGrammar)) { $this->useDefaultSchemaGrammar(); } @@ -81,7 +84,7 @@ public function getSchemaBuilder() /** * Get the default schema grammar instance. * - * @return \Illuminate\Database\Schema\Grammars\SqlServerGrammar + * @return \Illuminate\Database\Grammar */ protected function getDefaultSchemaGrammar() { @@ -101,10 +104,10 @@ protected function getDefaultPostProcessor() /** * Get the Doctrine DBAL driver. * - * @return \Doctrine\DBAL\Driver\PDOSqlsrv\Driver + * @return \Illuminate\Database\PDO\SqlServerDriver */ protected function getDoctrineDriver() { - return new DoctrineDriver; + return new SqlServerDriver; } } diff --git a/src/Database/Connectors/ConnectionFactory.php b/src/Database/Connectors/ConnectionFactory.php index 0e7f04a5e..629630037 100644 --- a/src/Database/Connectors/ConnectionFactory.php +++ b/src/Database/Connectors/ConnectionFactory.php @@ -32,7 +32,9 @@ protected function createPdoResolverWithHosts(array $config) } } - throw $e; + if (isset($e)) { + throw $e; + } }; } diff --git a/src/Database/DataFeed.php b/src/Database/DataFeed.php index fa77c442a..8d51a3f07 100644 --- a/src/Database/DataFeed.php +++ b/src/Database/DataFeed.php @@ -1,10 +1,9 @@ processCollection(); $bindings = $query->bindings; $records = sprintf("(%s) as records", $query->toSql()); - $result = Db::table(Db::raw($records))->selectRaw("COUNT(*) as total"); + $result = DB::table(DB::raw($records))->selectRaw("COUNT(*) as total"); // Set the bindings, if present foreach ($bindings as $type => $params) { @@ -135,8 +133,8 @@ public function get() */ $mixedArray = []; foreach ($records as $record) { - $tagName = $record->{$this->tagVar}; - $mixedArray[$tagName][] = $record->id; + $tagName = $record->getAttribute($this->tagVar); + $mixedArray[$tagName][] = $record->getKey(); } /* @@ -156,8 +154,8 @@ public function get() foreach ($records as $record) { $tagName = $record->{$this->tagVar}; - $obj = $collectionArray[$tagName]->find($record->id); - $obj->{$this->tagVar} = $tagName; + $obj = $collectionArray[$tagName]->find($record->getKey()); + $obj->setAttribute($this->tagVar, $tagName); $dataArray[] = $obj; } diff --git a/src/Database/DatabaseServiceProvider.php b/src/Database/DatabaseServiceProvider.php index 82477c423..de3db8c3f 100644 --- a/src/Database/DatabaseServiceProvider.php +++ b/src/Database/DatabaseServiceProvider.php @@ -55,6 +55,14 @@ public function register() return $app['db']->connection(); }); + $this->app->bind('db.schema', function ($app) { + $builder = $app['db']->connection()->getSchemaBuilder(); + + $app['events']->fire('db.schema.getBuilder', [$builder]); + + return $builder; + }); + $this->app->singleton('db.dongle', function ($app) { return new Dongle($this->getDefaultDatabaseDriver(), $app['db']); }); diff --git a/src/Database/MigrationServiceProvider.php b/src/Database/MigrationServiceProvider.php new file mode 100644 index 000000000..a1d81b329 --- /dev/null +++ b/src/Database/MigrationServiceProvider.php @@ -0,0 +1,15 @@ +save(null, $sessionKey); + $model->save([], $sessionKey); return $model; } @@ -148,6 +151,13 @@ protected function bootNicerEvents() { $class = get_called_class(); + // If the $dispatcher hasn't been set yet don't bother trying + // to register the nicer model events yet since it will silently fail + if (!isset(static::$dispatcher)) { + return; + } + + // Events have already been booted, continue if (isset(static::$eventsBooted[$class])) { return; } @@ -474,7 +484,7 @@ public static function fetched($callback) /** * Checks if an attribute is jsonable or not. * - * @return array + * @return bool */ public function isJsonable($key) { @@ -527,7 +537,7 @@ public function getObservableEvents() /** * Get a fresh timestamp for the model. * - * @return \Winter\Storm\Argon\Argon + * @return \Illuminate\Support\Carbon */ public function freshTimestamp() { @@ -602,10 +612,10 @@ protected function asDateTime($value) /** * Convert a DateTime to a storable string. * - * @param \DateTime|int $value - * @return string + * @param \DateTime|int|null $value + * @return string|null */ - public function fromDateTime($value) + public function fromDateTime($value = null) { if (is_null($value)) { return $value; @@ -618,7 +628,7 @@ public function fromDateTime($value) * Create a new Eloquent query builder for the model. * * @param \Winter\Storm\Database\QueryBuilder $query - * @return \Winter\Storm\Database\Builder|static + * @return \Winter\Storm\Database\Builder */ public function newEloquentBuilder($query) { @@ -667,7 +677,7 @@ public function __get($name) public function __set($name, $value) { - return $this->extendableSet($name, $value); + $this->extendableSet($name, $value); } public function __call($name, $params) @@ -702,7 +712,7 @@ public function __isset($key) * @param mixed $offset * @return bool */ - public function offsetExists($offset) + public function offsetExists($offset): bool { if ($result = parent::offsetExists($offset)) { return $result; @@ -728,7 +738,7 @@ public function newPivot(EloquentModel $parent, array $attributes, $table, $exis { return $using ? $using::fromRawAttributes($parent, $attributes, $table, $exists) - : new Pivot($parent, $attributes, $table, $exists); + : Pivot::fromAttributes($parent, $attributes, $table, $exists); } /** @@ -738,7 +748,7 @@ public function newPivot(EloquentModel $parent, array $attributes, $table, $exis * @param array $attributes * @param string $table * @param bool $exists - * @return \Winter\Storm\Database\Pivot + * @return \Winter\Storm\Database\Pivot|null */ public function newRelationPivot($relationName, $parent, $attributes, $table, $exists) { @@ -746,7 +756,7 @@ public function newRelationPivot($relationName, $parent, $attributes, $table, $e if (!is_null($definition) && array_key_exists('pivotModel', $definition)) { $pivotModel = $definition['pivotModel']; - return new $pivotModel($parent, $attributes, $table, $exists); + return $pivotModel::fromAttributes($parent, $attributes, $table, $exists); } } @@ -759,7 +769,7 @@ public function newRelationPivot($relationName, $parent, $attributes, $table, $e * @param array $options * @return bool */ - protected function saveInternal($options = []) + protected function saveInternal(array $options = []) { /** * @event model.saveInternal @@ -800,15 +810,6 @@ protected function saveInternal($options = []) return $result; } - /* - * If there is nothing to update, Eloquent will not fire afterSave(), - * events should still fire for consistency. - */ - if ($result === null) { - $this->fireModelEvent('updated', false); - $this->fireModelEvent('saved', false); - } - // Apply post deferred bindings if ($this->sessionKey !== null) { $this->commitDeferredAfter($this->sessionKey); @@ -820,10 +821,10 @@ protected function saveInternal($options = []) /** * Save the model to the database. * @param array $options - * @param null $sessionKey + * @param string|null $sessionKey * @return bool */ - public function save(array $options = null, $sessionKey = null) + public function save(?array $options = [], $sessionKey = null) { $this->sessionKey = $sessionKey; return $this->saveInternal(['force' => false] + (array) $options); @@ -831,15 +832,16 @@ public function save(array $options = null, $sessionKey = null) /** * Save the model and all of its relationships. + * * @param array $options - * @param null $sessionKey + * @param string|null $sessionKey * @return bool */ - public function push($options = null, $sessionKey = null) + public function push(?array $options = [], $sessionKey = null) { $always = Arr::get($options, 'always', false); - if (!$this->save(null, $sessionKey) && !$always) { + if (!$this->save([], $sessionKey) && !$always) { return false; } @@ -871,11 +873,12 @@ public function push($options = null, $sessionKey = null) /** * Pushes the first level of relations even if the parent * model has no changes. + * * @param array $options - * @param string $sessionKey + * @param string|null $sessionKey * @return bool */ - public function alwaysPush($options, $sessionKey) + public function alwaysPush(?array $options = [], $sessionKey = null) { return $this->push(['always' => true] + (array) $options, $sessionKey); } @@ -1198,7 +1201,7 @@ public function attributesToArray() * Set a given attribute on the model. * @param string $key * @param mixed $value - * @return void + * @return mixed|null */ public function setAttribute($key, $value) { @@ -1213,7 +1216,8 @@ public function setAttribute($key, $value) * Handle direct relation setting */ if ($this->hasRelation($key) && !$this->hasSetMutator($key)) { - return $this->setRelationValue($key, $value); + $this->setRelationValue($key, $value); + return; } /** diff --git a/src/Database/ModelInterface.php b/src/Database/ModelInterface.php new file mode 100644 index 000000000..3bad3e2e3 --- /dev/null +++ b/src/Database/ModelInterface.php @@ -0,0 +1,20 @@ +cast == 'date' && !is_null($value)) { + if ($this->getAttribute('cast') === 'date' && !is_null($value)) { return $this->asDateTime($value); } @@ -34,7 +33,7 @@ public function getNewValueAttribute($value) */ public function getOldValueAttribute($value) { - if ($this->cast == 'date' && !is_null($value)) { + if ($this->getAttribute('cast') === 'date' && !is_null($value)) { return $this->asDateTime($value); } diff --git a/src/Database/MorphPivot.php b/src/Database/MorphPivot.php new file mode 100644 index 000000000..de1b324e0 --- /dev/null +++ b/src/Database/MorphPivot.php @@ -0,0 +1,190 @@ +where($this->morphType, $this->morphClass); + + return parent::setKeysForSaveQuery($query); + } + + /** + * Set the keys for a select query. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function setKeysForSelectQuery($query) + { + $query->where($this->morphType, $this->morphClass); + + return parent::setKeysForSelectQuery($query); + } + + /** + * Delete the pivot model record from the database. + * + * @return int + */ + public function delete() + { + if (isset($this->attributes[$this->getKeyName()])) { + return (int) parent::delete(); + } + + if ($this->fireModelEvent('deleting') === false) { + return 0; + } + + $query = $this->getDeleteQuery(); + + $query->where($this->morphType, $this->morphClass); + + return tap($query->delete(), function () { + $this->fireModelEvent('deleted', false); + }); + } + + /** + * Get the morph type for the pivot. + * + * @return string + */ + public function getMorphType() + { + return $this->morphType; + } + + + /** + * Set the morph type for the pivot. + * + * @param string $morphType + * @return $this + */ + public function setMorphType($morphType) + { + $this->morphType = $morphType; + + return $this; + } + + /** + * Set the morph class for the pivot. + * + * @param string $morphClass + * @return \Winter\Storm\Database\MorphPivot + */ + public function setMorphClass($morphClass) + { + $this->morphClass = $morphClass; + + return $this; + } + + + /** + * Get the queueable identity for the entity. + * + * @return mixed + */ + public function getQueueableId() + { + if (isset($this->attributes[$this->getKeyName()])) { + return $this->getKey(); + } + + return sprintf( + '%s:%s:%s:%s:%s:%s', + $this->foreignKey, + $this->getAttribute($this->foreignKey), + $this->relatedKey, + $this->getAttribute($this->relatedKey), + $this->morphType, + $this->morphClass + ); + } + + /** + * Get a new query to restore one or more models by their queueable IDs. + * + * @param array|int $ids + * @return \Illuminate\Database\Eloquent\Builder + */ + public function newQueryForRestoration($ids) + { + if (is_array($ids)) { + return $this->newQueryForCollectionRestoration($ids); + } + + if (!str_contains($ids, ':')) { + return parent::newQueryForRestoration($ids); + } + + $segments = explode(':', $ids); + + return $this->newQueryWithoutScopes() + ->where($segments[0], $segments[1]) + ->where($segments[2], $segments[3]) + ->where($segments[4], $segments[5]); + } + + /** + * Get a new query to restore multiple models by their queueable IDs. + * + * @param array $ids + * @return \Illuminate\Database\Eloquent\Builder + */ + protected function newQueryForCollectionRestoration(array $ids) + { + $ids = array_values($ids); + + if (!str_contains($ids[0], ':')) { + return parent::newQueryForRestoration($ids); + } + + $query = $this->newQueryWithoutScopes(); + + foreach ($ids as $id) { + $segments = explode(':', $id); + + $query->orWhere(function ($query) use ($segments) { + return $query->where($segments[0], $segments[1]) + ->where($segments[2], $segments[3]) + ->where($segments[4], $segments[5]); + }); + } + + return $query; + } +} diff --git a/src/Database/Pivot.php b/src/Database/Pivot.php index 5172b7df2..01538273c 100644 --- a/src/Database/Pivot.php +++ b/src/Database/Pivot.php @@ -1,170 +1,48 @@ setRawAttributes($attributes, true); - - $this->setTable($table); - - $this->setConnection($parent->getConnectionName()); - - // We store off the parent instance so we will access the timestamp column names - // for the model, since the pivot model timestamps aren't easily configurable - // from the developer's point of view. We can use the parents to get these. - $this->parent = $parent; - - $this->exists = $exists; - - $this->timestamps = $this->hasTimestampAttributes(); - } - - /** - * Set the keys for a save update query. - * - * @param \Illuminate\Database\Eloquent\Builder - * @return \Illuminate\Database\Eloquent\Builder - */ - protected function setKeysForSaveQuery(BuilderBase $query) - { - $query->where($this->foreignKey, $this->getAttribute($this->foreignKey)); - - return $query->where($this->otherKey, $this->getAttribute($this->otherKey)); - } - - /** - * Delete the pivot model record from the database. - * - * @return int - */ - public function delete() - { - return $this->getDeleteQuery()->delete(); - } - - /** - * Get the query builder for a delete operation on the pivot. + * Indicates if the IDs are auto-incrementing. * - * @return \Illuminate\Database\Eloquent\Builder + * @var bool */ - protected function getDeleteQuery() - { - $foreign = $this->getAttribute($this->foreignKey); - - $query = $this->newQuery()->where($this->foreignKey, $foreign); - - return $query->where($this->otherKey, $this->getAttribute($this->otherKey)); - } + public $incrementing = false; /** - * Get the foreign key column name. + * Gets the parent attribute. * - * @return string - */ - public function getForeignKey() - { - return $this->foreignKey; - } - - /** - * Get the "other key" column name. + * Provided for backwards-compatibility. * - * @return string + * @param mixed $value + * @return \Illuminate\Database\Eloquent\Model|null */ - public function getOtherKey() + public function getParentAttribute($value) { - return $this->otherKey; + return $this->pivotParent; } /** - * Set the key names for the pivot model instance. + * Sets the parent attribute. * - * @param string $foreignKey - * @param string $otherKey - * @return $this - */ - public function setPivotKeys($foreignKey, $otherKey) - { - $this->foreignKey = $foreignKey; - - $this->otherKey = $otherKey; - - return $this; - } - - /** - * Determine if the pivot model has timestamp attributes. - * - * @return bool - */ - public function hasTimestampAttributes() - { - return array_key_exists($this->getCreatedAtColumn(), $this->attributes); - } - - /** - * Get the name of the "created at" column. - * - * @return string - */ - public function getCreatedAtColumn() - { - return $this->parent->getCreatedAtColumn(); - } - - /** - * Get the name of the "updated at" column. + * Provided for backwards-compatibility. * - * @return string + * @param \Illuminate\Database\Eloquent\Model $value + * @return void */ - public function getUpdatedAtColumn() + public function setParentAttribute($value) { - return $this->parent->getUpdatedAtColumn(); + $this->pivotParent = $value; } } diff --git a/src/Database/Query/Grammars/MySqlGrammar.php b/src/Database/Query/Grammars/MySqlGrammar.php index 0b9501089..5964e8b8f 100644 --- a/src/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Database/Query/Grammars/MySqlGrammar.php @@ -1,32 +1,9 @@ compileInsert($query, $values) . ' on duplicate key update '; - - $columns = collect($update)->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value) . ' = values(' . $this->wrap($value) . ')' - : $this->wrap($key) . ' = ' . $this->parameter($value); - })->implode(', '); - - return $sql . $columns; - } } diff --git a/src/Database/Query/Grammars/PostgresGrammar.php b/src/Database/Query/Grammars/PostgresGrammar.php index 2b8a167c3..58ad779ec 100644 --- a/src/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Database/Query/Grammars/PostgresGrammar.php @@ -1,34 +1,9 @@ compileInsert($query, $values); - - $sql .= ' on conflict (' . $this->columnize($uniqueBy) . ') do update set '; - - $columns = collect($update)->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value) . ' = ' . $this->wrapValue('excluded') . '.' . $this->wrap($value) - : $this->wrap($key) . ' = ' . $this->parameter($value); - })->implode(', '); - - return $sql . $columns; - } } diff --git a/src/Database/Query/Grammars/SQLiteGrammar.php b/src/Database/Query/Grammars/SQLiteGrammar.php index 942612e1f..4cf7fb76f 100644 --- a/src/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Database/Query/Grammars/SQLiteGrammar.php @@ -1,6 +1,5 @@ wrap($as); } - - /** - * Compile an "upsert" statement into SQL. - * - * @param \Winter\Storm\Database\QueryBuilder $query - * @param array $values - * @param array $uniqueBy - * @param array $update - * @return string - */ - public function compileUpsert(QueryBuilder $query, array $values, array $uniqueBy, array $update) - { - $sql = $this->compileInsert($query, $values); - - $sql .= ' on conflict (' . $this->columnize($uniqueBy) . ') do update set '; - - $columns = collect($update)->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value) . ' = ' . $this->wrapValue('excluded') . '.' . $this->wrap($value) - : $this->wrap($key) . ' = ' . $this->parameter($value); - })->implode(', '); - - return $sql . $columns; - } } diff --git a/src/Database/Query/Grammars/SqlServerGrammar.php b/src/Database/Query/Grammars/SqlServerGrammar.php index ac70ec53c..17cb26c97 100644 --- a/src/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Database/Query/Grammars/SqlServerGrammar.php @@ -1,52 +1,9 @@ columnize(array_keys(reset($values))); - - $sql = 'merge ' . $this->wrapTable($query->from) . ' '; - - $parameters = collect($values)->map(function ($record) { - return '(' . $this->parameterize($record) . ')'; - })->implode(', '); - - $sql .= 'using (values ' . $parameters . ') ' . $this->wrapTable('laravel_source') . ' (' . $columns . ') '; - - $on = collect($uniqueBy)->map(function ($column) use ($query) { - return $this->wrap('laravel_source.' . $column) . ' = ' . $this->wrap($query->from . '.' . $column); - })->implode(' and '); - - $sql .= 'on ' . $on . ' '; - - if ($update) { - $update = collect($update)->map(function ($value, $key) { - return is_numeric($key) - ? $this->wrap($value) . ' = ' . $this->wrap('laravel_source.' . $value) - : $this->wrap($key) . ' = ' . $this->parameter($value); - })->implode(', '); - - $sql .= 'when matched then update set ' . $update . ' '; - } - - $sql .= 'when not matched then insert (' . $columns . ') values (' . $columns . ')'; - - return $sql; - } } diff --git a/src/Database/QueryBuilder.php b/src/Database/QueryBuilder.php index ecdaea81b..9126576ad 100644 --- a/src/Database/QueryBuilder.php +++ b/src/Database/QueryBuilder.php @@ -1,7 +1,8 @@ columns)) { - $this->columns = $columns; - } - $cache = MemoryCache::instance(); if ($cache->has($this)) { @@ -140,14 +137,10 @@ protected function getDuplicateCached($columns = ['*']) * Execute the query as a cached "select" statement. * * @param array $columns - * @return array + * @return BaseCollection */ public function getCached($columns = ['*']) { - if (is_null($this->columns)) { - $this->columns = $columns; - } - // If the query is requested to be cached, we will cache it using a unique key // for this database connection and query statement, including the bindings // that are used on this query, providing great convenience when caching. @@ -237,9 +230,8 @@ protected function getCacheCallback($columns) * also strips off any orderBy clause. * * @param string $columns - * @return int */ - public function count($columns = '*') + public function count($columns = '*'): int { $previousOrders = $this->orders; @@ -389,10 +381,8 @@ public function flushDuplicateCache() /** * Enable the memory cache on the query. - * - * @return \Illuminate\Database\Query\Builder|static */ - public function enableDuplicateCache() + public function enableDuplicateCache(): static { $this->cachingDuplicateQueries = true; @@ -401,10 +391,8 @@ public function enableDuplicateCache() /** * Disable the memory cache on the query. - * - * @return \Illuminate\Database\Query\Builder|static */ - public function disableDuplicateCache() + public function disableDuplicateCache(): static { $this->cachingDuplicateQueries = false; @@ -468,7 +456,7 @@ protected function runPaginationCountQuery($columns = ['*']) if ($this->groups || $this->havings) { $clone = $this->cloneForPaginationCount(); - if (is_null($clone->columns) && !empty($this->joins)) { + if (empty($clone->columns) && !empty($this->joins)) { $clone->select($this->from . '.*'); } diff --git a/src/Database/Relations/AttachMany.php b/src/Database/Relations/AttachMany.php index 856978f6f..06e0005b1 100644 --- a/src/Database/Relations/AttachMany.php +++ b/src/Database/Relations/AttachMany.php @@ -1,25 +1,27 @@ getPath(); } } diff --git a/src/Database/Relations/AttachOne.php b/src/Database/Relations/AttachOne.php index c8687cc2f..33a676b72 100644 --- a/src/Database/Relations/AttachOne.php +++ b/src/Database/Relations/AttachOne.php @@ -5,21 +5,23 @@ use Illuminate\Database\Eloquent\Relations\MorphOne as MorphOneBase; use Winter\Storm\Database\Attach\File as FileModel; +/** + * @phpstan-property \Winter\Storm\Database\Model $parent + */ class AttachOne extends MorphOneBase { - use AttachOneOrMany; - use DefinedConstraints; + use Concerns\AttachOneOrMany; + use Concerns\DefinedConstraints; /** * Create a new has many relationship instance. * @param Builder $query * @param Model $parent - * @param $type - * @param $id - * @param $isPublic - * @param $localKey + * @param string $type + * @param string $id + * @param bool $isPublic + * @param string $localKey * @param null|string $relationName - * @param null|string $keyType */ public function __construct(Builder $query, Model $parent, $type, $id, $isPublic, $localKey, $relationName = null) { diff --git a/src/Database/Relations/BelongsTo.php b/src/Database/Relations/BelongsTo.php index 370102ee7..798906eac 100644 --- a/src/Database/Relations/BelongsTo.php +++ b/src/Database/Relations/BelongsTo.php @@ -4,10 +4,13 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\BelongsTo as BelongsToBase; +/** + * @phpstan-property \Winter\Storm\Database\Model $child + */ class BelongsTo extends BelongsToBase { - use DeferOneOrMany; - use DefinedConstraints; + use Concerns\DeferOneOrMany; + use Concerns\DefinedConstraints; /** * @var string The "name" of the relationship. diff --git a/src/Database/Relations/BelongsToMany.php b/src/Database/Relations/BelongsToMany.php index f498c3641..ec140847c 100644 --- a/src/Database/Relations/BelongsToMany.php +++ b/src/Database/Relations/BelongsToMany.php @@ -1,427 +1,10 @@ addDefinedConstraints(); - } - - /** - * Get the select columns for the relation query. - * - * @param array $columns - * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany - */ - protected function shouldSelect(array $columns = ['*']) - { - if ($this->countMode) { - return $this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->foreignPivotKey; - } - - if ($columns == ['*']) { - $columns = [$this->related->getTable().'.*']; - } - - if ($this->orphanMode) { - return $columns; - } - - return array_merge($columns, $this->aliasedPivotColumns()); - } - - /** - * Save the supplied related model with deferred binding support. - */ - public function save(Model $model, array $pivotData = [], $sessionKey = null) - { - $model->save(); - $this->add($model, $sessionKey, $pivotData); - return $model; - } - - /** - * Override sync() method of BelongToMany relation in order to flush the query cache. - * @param array $ids - * @param bool $detaching - * @return array - */ - public function sync($ids, $detaching = true) - { - $changed = parent::sync($ids, $detaching); - - $this->flushDuplicateCache(); - - return $changed; - } - - /** - * Create a new instance of this related model with deferred binding support. - */ - public function create(array $attributes = [], array $pivotData = [], $sessionKey = null) - { - $model = $this->related->create($attributes); - - $this->add($model, $sessionKey, $pivotData); - - return $model; - } - - /** - * Override attach() method of BelongToMany relation. - * This is necessary in order to fire 'model.relation.beforeAttach', 'model.relation.afterAttach' events - * @param mixed $id - * @param array $attributes - * @param bool $touch - */ - public function attach($id, array $attributes = [], $touch = true) - { - $insertData = $this->formatAttachRecords($this->parseIds($id), $attributes); - $attachedIdList = array_pluck($insertData, $this->relatedPivotKey); - - /** - * @event model.relation.beforeAttach - * Called before creating a new relation between models (only for BelongsToMany relation) - * - * Example usage: - * - * $model->bindEvent('model.relation.beforeAttach', function (string $relationName, array $attachedIdList, array $insertData) use (\Winter\Storm\Database\Model $model) { - * if (!$model->isRelationValid($attachedIdList)) { - * throw new \Exception("Invalid relation!"); - * return false; - * } - * }); - * - */ - if ($this->parent->fireEvent('model.relation.beforeAttach', [$this->relationName, $attachedIdList, $insertData], true) === false) { - return; - } - - // Here we will insert the attachment records into the pivot table. Once we have - // inserted the records, we will touch the relationships if necessary and the - // function will return. We can parse the IDs before inserting the records. - $this->newPivotStatement()->insert($insertData); - - if ($touch) { - $this->touchIfTouching(); - } - - /** - * @event model.relation.afterAttach - * Called after creating a new relation between models (only for BelongsToMany relation) - * - * Example usage: - * - * $model->bindEvent('model.relation.afterAttach', function (string $relationName, array $attachedIdList, array $insertData) use (\Winter\Storm\Database\Model $model) { - * traceLog("New relation {$relationName} was created", $attachedIdList); - * }); - * - */ - $this->parent->fireEvent('model.relation.afterAttach', [$this->relationName, $attachedIdList, $insertData]); - } - - /** - * Override detach() method of BelongToMany relation. - * This is necessary in order to fire 'model.relation.beforeDetach', 'model.relation.afterDetach' events - * @param null $ids - * @param bool $touch - * @return int|void - */ - public function detach($ids = null, $touch = true) - { - $attachedIdList = $this->parseIds($ids); - if (empty($attachedIdList)) { - $attachedIdList = $this->newPivotQuery()->lists($this->relatedPivotKey); - } - - /** - * @event model.relation.beforeDetach - * Called before removing a relation between models (only for BelongsToMany relation) - * - * Example usage: - * - * $model->bindEvent('model.relation.beforeDetach', function (string $relationName, array $attachedIdList) use (\Winter\Storm\Database\Model $model) { - * if (!$model->isRelationValid($attachedIdList)) { - * throw new \Exception("Invalid relation!"); - * return false; - * } - * }); - * - */ - if ($this->parent->fireEvent('model.relation.beforeDetach', [$this->relationName, $attachedIdList], true) === false) { - return; - } - - /** - * @see Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable - */ - parent::detach($attachedIdList, $touch); - - /** - * @event model.relation.afterDetach - * Called after removing a relation between models (only for BelongsToMany relation) - * - * Example usage: - * - * $model->bindEvent('model.relation.afterDetach', function (string $relationName, array $attachedIdList) use (\Winter\Storm\Database\Model $model) { - * traceLog("Relation {$relationName} was removed", $attachedIdList); - * }); - * - */ - $this->parent->fireEvent('model.relation.afterDetach', [$this->relationName, $attachedIdList]); - } - - /** - * Adds a model to this relationship type. - */ - public function add(Model $model, $sessionKey = null, $pivotData = []) - { - if (is_array($sessionKey)) { - $pivotData = $sessionKey; - $sessionKey = null; - } - - if ($sessionKey === null || $sessionKey === false) { - $this->attach($model->getKey(), $pivotData); - $this->parent->reloadRelations($this->relationName); - } - else { - $this->parent->bindDeferred($this->relationName, $model, $sessionKey, $pivotData); - } - } - - /** - * Removes a model from this relationship type. - */ - public function remove(Model $model, $sessionKey = null) - { - if ($sessionKey === null) { - $this->detach($model->getKey()); - $this->parent->reloadRelations($this->relationName); - } - else { - $this->parent->unbindDeferred($this->relationName, $model, $sessionKey); - } - } - - /** - * Get a paginator for the "select" statement. Complies with Winter Storm. - * - * @param int $perPage - * @param int $currentPage - * @param array $columns - * @param string $pageName - * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator - */ - public function paginate($perPage = 15, $currentPage = null, $columns = ['*'], $pageName = 'page') - { - $this->query->addSelect($this->shouldSelect($columns)); - - $paginator = $this->query->paginate($perPage, $currentPage, $columns); - - $this->hydratePivotRelation($paginator->items()); - - return $paginator; - } - - /** - * Create a new pivot model instance. - * - * @param array $attributes - * @param bool $exists - * @return \Illuminate\Database\Eloquent\Relations\Pivot - */ - public function newPivot(array $attributes = [], $exists = false) - { - /* - * Winter looks to the relationship parent - */ - $pivot = $this->parent->newRelationPivot($this->relationName, $this->parent, $attributes, $this->table, $exists); - - /* - * Laravel looks to the related model - */ - if (empty($pivot)) { - $pivot = $this->related->newPivot($this->parent, $attributes, $this->table, $exists); - } - - return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey); - } - - /** - * Helper for setting this relationship using various expected - * values. For example, $model->relation = $value; - */ - public function setSimpleValue($value) - { - $relationModel = $this->getRelated(); - - /* - * Nulling the relationship - */ - if (!$value) { - // Disassociate in memory immediately - $this->parent->setRelation($this->relationName, $relationModel->newCollection()); - - // Perform sync when the model is saved - $this->parent->bindEventOnce('model.afterSave', function () use ($value) { - $this->detach(); - }); - return; - } - - /* - * Convert models to keys - */ - if ($value instanceof Model) { - $value = $value->getKey(); - } - elseif (is_array($value)) { - foreach ($value as $_key => $_value) { - if ($_value instanceof Model) { - $value[$_key] = $_value->getKey(); - } - } - } - - /* - * Convert scalar to array - */ - if (!is_array($value) && !$value instanceof CollectionBase) { - $value = [$value]; - } - - /* - * Setting the relationship - */ - $relationCollection = $value instanceof CollectionBase - ? $value - : $relationModel->whereIn($relationModel->getKeyName(), $value)->get(); - - // Associate in memory immediately - $this->parent->setRelation($this->relationName, $relationCollection); - - // Perform sync when the model is saved - $this->parent->bindEventOnce('model.afterSave', function () use ($value) { - $this->sync($value); - }); - } - - /** - * Helper for getting this relationship simple value, - * generally useful with form values. - */ - public function getSimpleValue() - { - $value = []; - - $relationName = $this->relationName; - - $sessionKey = $this->parent->sessionKey; - - if ($this->parent->relationLoaded($relationName)) { - $related = $this->getRelated(); - - $value = $this->parent->getRelation($relationName)->pluck($related->getKeyName())->all(); - } - else { - $value = $this->allRelatedIds($sessionKey)->all(); - } - - return $value; - } - - /** - * Get all of the IDs for the related models, with deferred binding support - * - * @param string $sessionKey - * @return \Winter\Storm\Support\Collection - */ - public function allRelatedIds($sessionKey = null) - { - $related = $this->getRelated(); - - $fullKey = $related->getQualifiedKeyName(); - - $query = $sessionKey ? $this->withDeferred($sessionKey) : $this; - - return $query->getQuery()->select($fullKey)->pluck($related->getKeyName()); - } - - /** - * Get the fully qualified foreign key for the relation. - * - * @return string - */ - public function getForeignKey() - { - return $this->table.'.'.$this->foreignPivotKey; - } - - /** - * Get the fully qualified "other key" for the relation. - * - * @return string - */ - public function getOtherKey() - { - return $this->table.'.'.$this->relatedPivotKey; - } - - /** - * @deprecated Use allRelatedIds instead. Remove if year >= 2018. - */ - public function getRelatedIds($sessionKey = null) - { - traceLog('Method BelongsToMany::getRelatedIds has been deprecated, use BelongsToMany::allRelatedIds instead.'); - return $this->allRelatedIds($sessionKey)->all(); - } + use Concerns\BelongsOrMorphsToMany; + use Concerns\DeferOneOrMany; + use Concerns\DefinedConstraints; } diff --git a/src/Database/Relations/AttachOneOrMany.php b/src/Database/Relations/Concerns/AttachOneOrMany.php similarity index 95% rename from src/Database/Relations/AttachOneOrMany.php rename to src/Database/Relations/Concerns/AttachOneOrMany.php index 0dec41d28..441e6b218 100644 --- a/src/Database/Relations/AttachOneOrMany.php +++ b/src/Database/Relations/Concerns/AttachOneOrMany.php @@ -1,10 +1,11 @@ -public) && $this->public !== null) { + if (isset($this->public)) { return $this->public; } @@ -117,7 +118,7 @@ public function save(Model $model, $sessionKey = null) $this->delete(); } - if (!array_key_exists('is_public', $model->attributes)) { + if (!array_key_exists('is_public', $model->getAttributes())) { $model->setAttribute('is_public', $this->isPublic()); } @@ -161,8 +162,8 @@ public function create(array $attributes = [], $sessionKey = null) */ public function add(Model $model, $sessionKey = null) { - if (!array_key_exists('is_public', $model->attributes)) { - $model->is_public = $this->isPublic(); + if (!array_key_exists('is_public', $model->getAttributes())) { + $model->setAttribute('is_public', $this->isPublic()); } if ($sessionKey === null) { @@ -270,7 +271,6 @@ public function makeValidationFile($value) $value->getLocalPath(), $value->file_name, $value->content_type, - $value->file_size, null, true ); diff --git a/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php b/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php new file mode 100644 index 000000000..7e346d3d6 --- /dev/null +++ b/src/Database/Relations/Concerns/BelongsOrMorphsToMany.php @@ -0,0 +1,422 @@ +addDefinedConstraints(); + } + + /** + * Get the select columns for the relation query. + * + * @param array $columns + * @return array|string + */ + protected function shouldSelect(array $columns = ['*']) + { + if ($this->countMode) { + return $this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->foreignPivotKey; + } + + if ($columns == ['*']) { + $columns = [$this->related->getTable().'.*']; + } + + if ($this->orphanMode) { + return $columns; + } + + return array_merge($columns, $this->aliasedPivotColumns()); + } + + /** + * Save the supplied related model with deferred binding support. + */ + public function save(Model $model, array $pivotData = [], $sessionKey = null) + { + $model->save(); + $this->add($model, $sessionKey, $pivotData); + return $model; + } + + /** + * Override sync() method of BelongToMany relation in order to flush the query cache. + * @param array $ids + * @param bool $detaching + * @return array + */ + public function sync($ids, $detaching = true) + { + $changed = parent::sync($ids, $detaching); + + $this->flushDuplicateCache(); + + return $changed; + } + + /** + * Create a new instance of this related model with deferred binding support. + */ + public function create(array $attributes = [], array $pivotData = [], $sessionKey = null) + { + $model = $this->related->create($attributes); + + $this->add($model, $sessionKey, $pivotData); + + return $model; + } + + /** + * Override attach() method of BelongToMany relation. + * This is necessary in order to fire 'model.relation.beforeAttach', 'model.relation.afterAttach' events + * @param mixed $id + * @param array $attributes + * @param bool $touch + */ + public function attach($id, array $attributes = [], $touch = true) + { + $insertData = $this->formatAttachRecords($this->parseIds($id), $attributes); + $attachedIdList = array_pluck($insertData, $this->relatedPivotKey); + + /** + * @event model.relation.beforeAttach + * Called before creating a new relation between models (only for BelongsToMany relation) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeAttach', function (string $relationName, array $attachedIdList, array $insertData) use (\Winter\Storm\Database\Model $model) { + * if (!$model->isRelationValid($attachedIdList)) { + * throw new \Exception("Invalid relation!"); + * return false; + * } + * }); + * + */ + if ($this->parent->fireEvent('model.relation.beforeAttach', [$this->relationName, $attachedIdList, $insertData], true) === false) { + return; + } + + // Here we will insert the attachment records into the pivot table. Once we have + // inserted the records, we will touch the relationships if necessary and the + // function will return. We can parse the IDs before inserting the records. + $this->newPivotStatement()->insert($insertData); + + if ($touch) { + $this->touchIfTouching(); + } + + /** + * @event model.relation.afterAttach + * Called after creating a new relation between models (only for BelongsToMany relation) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterAttach', function (string $relationName, array $attachedIdList, array $insertData) use (\Winter\Storm\Database\Model $model) { + * traceLog("New relation {$relationName} was created", $attachedIdList); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterAttach', [$this->relationName, $attachedIdList, $insertData]); + } + + /** + * Override detach() method of BelongToMany relation. + * This is necessary in order to fire 'model.relation.beforeDetach', 'model.relation.afterDetach' events + * @param Collection|Model|array|null $ids + * @param bool $touch + * @return int|void + */ + public function detach($ids = null, $touch = true) + { + $attachedIdList = $this->parseIds($ids); + if (empty($attachedIdList)) { + $attachedIdList = $this->newPivotQuery()->lists($this->relatedPivotKey); + } + + /** + * @event model.relation.beforeDetach + * Called before removing a relation between models (only for BelongsToMany relation) + * + * Example usage: + * + * $model->bindEvent('model.relation.beforeDetach', function (string $relationName, array $attachedIdList) use (\Winter\Storm\Database\Model $model) { + * if (!$model->isRelationValid($attachedIdList)) { + * throw new \Exception("Invalid relation!"); + * return false; + * } + * }); + * + */ + if ($this->parent->fireEvent('model.relation.beforeDetach', [$this->relationName, $attachedIdList], true) === false) { + return; + } + + /** + * @see Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable + */ + parent::detach($attachedIdList, $touch); + + /** + * @event model.relation.afterDetach + * Called after removing a relation between models (only for BelongsToMany relation) + * + * Example usage: + * + * $model->bindEvent('model.relation.afterDetach', function (string $relationName, array $attachedIdList) use (\Winter\Storm\Database\Model $model) { + * traceLog("Relation {$relationName} was removed", $attachedIdList); + * }); + * + */ + $this->parent->fireEvent('model.relation.afterDetach', [$this->relationName, $attachedIdList]); + } + + /** + * Adds a model to this relationship type. + */ + public function add(Model $model, $sessionKey = null, $pivotData = []) + { + if (is_array($sessionKey)) { + $pivotData = $sessionKey; + $sessionKey = null; + } + + if ($sessionKey === null || $sessionKey === false) { + $this->attach($model->getKey(), $pivotData); + $this->parent->reloadRelations($this->relationName); + } + else { + $this->parent->bindDeferred($this->relationName, $model, $sessionKey, $pivotData); + } + } + + /** + * Removes a model from this relationship type. + */ + public function remove(Model $model, $sessionKey = null) + { + if ($sessionKey === null) { + $this->detach($model->getKey()); + $this->parent->reloadRelations($this->relationName); + } + else { + $this->parent->unbindDeferred($this->relationName, $model, $sessionKey); + } + } + + /** + * Get a paginator for the "select" statement. Complies with Winter Storm. + * + * @param int $perPage + * @param int $currentPage + * @param array $columns + * @param string $pageName + * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator + */ + public function paginate($perPage = 15, $currentPage = null, $columns = ['*'], $pageName = 'page') + { + $this->query->addSelect($this->shouldSelect($columns)); + + $paginator = $this->query->paginate($perPage, $currentPage, $columns); + + $this->hydratePivotRelation($paginator->items()); + + return $paginator; + } + + /** + * Create a new pivot model instance. + * + * @param array $attributes + * @param bool $exists + * @return \Illuminate\Database\Eloquent\Relations\Pivot + */ + public function newPivot(array $attributes = [], $exists = false) + { + /* + * Winter looks to the relationship parent + */ + $pivot = $this->parent->newRelationPivot($this->relationName, $this->parent, $attributes, $this->table, $exists); + + /* + * Laravel looks to the related model + */ + if (empty($pivot)) { + $pivot = $this->related->newPivot($this->parent, $attributes, $this->table, $exists); + } + + return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey); + } + + /** + * Helper for setting this relationship using various expected + * values. For example, $model->relation = $value; + */ + public function setSimpleValue($value) + { + $relationModel = $this->getRelated(); + + /* + * Nulling the relationship + */ + if (!$value) { + // Disassociate in memory immediately + $this->parent->setRelation($this->relationName, $relationModel->newCollection()); + + // Perform sync when the model is saved + $this->parent->bindEventOnce('model.afterSave', function () { + $this->detach(); + }); + return; + } + + /* + * Convert models to keys + */ + if ($value instanceof Model) { + $value = $value->getKey(); + } + elseif (is_array($value)) { + foreach ($value as $_key => $_value) { + if ($_value instanceof Model) { + $value[$_key] = $_value->getKey(); + } + } + } + + /* + * Convert scalar to array + */ + if (!is_array($value) && !$value instanceof Collection) { + $value = [$value]; + } + + /* + * Setting the relationship + */ + $relationCollection = $value instanceof Collection + ? $value + : $relationModel->whereIn($relationModel->getKeyName(), $value)->get(); + + // Associate in memory immediately + $this->parent->setRelation($this->relationName, $relationCollection); + + // Perform sync when the model is saved + $this->parent->bindEventOnce('model.afterSave', function () use ($value) { + $this->sync($value); + }); + } + + /** + * Helper for getting this relationship simple value, + * generally useful with form values. + */ + public function getSimpleValue() + { + $value = []; + + $relationName = $this->relationName; + + $sessionKey = $this->parent->sessionKey; + + if ($this->parent->relationLoaded($relationName)) { + $related = $this->getRelated(); + + $value = $this->parent->getRelation($relationName)->pluck($related->getKeyName())->all(); + } + else { + $value = $this->allRelatedIds($sessionKey)->all(); + } + + return $value; + } + + /** + * Get all of the IDs for the related models, with deferred binding support + * + * @param string $sessionKey + * @return \Illuminate\Support\Collection + */ + public function allRelatedIds($sessionKey = null) + { + $related = $this->getRelated(); + + $fullKey = $related->getQualifiedKeyName(); + + $query = $sessionKey ? $this->withDeferred($sessionKey) : $this; + + return $query->getQuery()->select($fullKey)->pluck($related->getKeyName()); + } + + /** + * Get the fully qualified foreign key for the relation. + * + * @return string + */ + public function getForeignKey() + { + return $this->table.'.'.$this->foreignPivotKey; + } + + /** + * Get the fully qualified "other key" for the relation. + * + * @return string + */ + public function getOtherKey() + { + return $this->table.'.'.$this->relatedPivotKey; + } + + /** + * @deprecated Use allRelatedIds instead. Remove if year >= 2018. + */ + public function getRelatedIds($sessionKey = null) + { + traceLog('Method BelongsToMany::getRelatedIds has been deprecated, use BelongsToMany::allRelatedIds instead.'); + return $this->allRelatedIds($sessionKey)->all(); + } +} diff --git a/src/Database/Relations/DeferOneOrMany.php b/src/Database/Relations/Concerns/DeferOneOrMany.php similarity index 83% rename from src/Database/Relations/DeferOneOrMany.php rename to src/Database/Relations/Concerns/DeferOneOrMany.php index 2ff605b7f..519c9d8ba 100644 --- a/src/Database/Relations/DeferOneOrMany.php +++ b/src/Database/Relations/Concerns/DeferOneOrMany.php @@ -1,13 +1,14 @@ -orphanMode = true; } $newQuery->where(function ($query) use ($sessionKey) { if ($this->parent->exists) { + /** @phpstan-ignore-next-line */ if ($this instanceof MorphToMany) { /* * Custom query for MorphToMany since a "join" cannot be used @@ -35,24 +38,27 @@ public function withDeferred($sessionKey) $query ->select($this->parent->getConnection()->raw(1)) ->from($this->table) - ->where($this->getOtherKey(), DbDongle::raw(DbDongle::getTablePrefix().$this->related->getQualifiedKeyName())) + ->where($this->getOtherKey(), DbDongle::raw( + DbDongle::getTablePrefix() . $this->related->getQualifiedKeyName() + )) ->where($this->getForeignKey(), $this->parent->getKey()) ->where($this->getMorphType(), $this->getMorphClass()); }); - } - elseif ($this instanceof BelongsToManyBase) { + /** @phpstan-ignore-next-line */ + } elseif ($this instanceof BelongsToMany) { /* - * Custom query for BelongsToManyBase since a "join" cannot be used + * Custom query for BelongsToMany since a "join" cannot be used */ $query->whereExists(function ($query) { $query ->select($this->parent->getConnection()->raw(1)) ->from($this->table) - ->where($this->getOtherKey(), DbDongle::raw(DbDongle::getTablePrefix().$this->related->getQualifiedKeyName())) + ->where($this->getOtherKey(), DbDongle::raw( + DbDongle::getTablePrefix() . $this->related->getQualifiedKeyName() + )) ->where($this->getForeignKey(), $this->parent->getKey()); }); - } - else { + } else { /* * Trick the relation to add constraints to this nested query */ diff --git a/src/Database/Relations/DefinedConstraints.php b/src/Database/Relations/Concerns/DefinedConstraints.php similarity index 86% rename from src/Database/Relations/DefinedConstraints.php rename to src/Database/Relations/Concerns/DefinedConstraints.php index 21b1bf7fc..3df5fd83b 100644 --- a/src/Database/Relations/DefinedConstraints.php +++ b/src/Database/Relations/Concerns/DefinedConstraints.php @@ -1,4 +1,4 @@ -parent->getRelationDefinition($this->relationName); @@ -76,10 +76,10 @@ public function addDefinedConstraintsToRelation($relation, $args = null) /** * Add query based constraints. * - * @param Winter\Storm\Database\QueryBuilder $query - * @param array $args + * @param \Illuminate\Database\Eloquent\Relations\Relation|\Winter\Storm\Database\QueryBuilder $query + * @param array|null $args */ - public function addDefinedConstraintsToQuery($query, $args = null) + public function addDefinedConstraintsToQuery($query, ?array $args = null) { if ($args === null) { $args = $this->parent->getRelationDefinition($this->relationName); diff --git a/src/Database/Relations/HasOneOrMany.php b/src/Database/Relations/Concerns/HasOneOrMany.php similarity index 97% rename from src/Database/Relations/HasOneOrMany.php rename to src/Database/Relations/Concerns/HasOneOrMany.php index 82ff74446..24cb271e8 100644 --- a/src/Database/Relations/HasOneOrMany.php +++ b/src/Database/Relations/Concerns/HasOneOrMany.php @@ -1,6 +1,7 @@ -addDefinedConstraints(); } - /** - * Get the results of the relationship. - * @return mixed - */ - public function getResults() - { - // New models have no possibility of having a relationship here - // so prevent the first orphaned relation from being used. - if (!$this->parent->exists) { - return null; - } - - return parent::getResults(); - } - /** * Helper for setting this relationship using various expected * values. For example, $model->relation = $value; diff --git a/src/Database/Relations/HasOneThrough.php b/src/Database/Relations/HasOneThrough.php index 8a4497a59..efcfcfab4 100644 --- a/src/Database/Relations/HasOneThrough.php +++ b/src/Database/Relations/HasOneThrough.php @@ -4,9 +4,13 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\HasOneThrough as HasOneThroughBase; +/** + * @phpstan-property \Winter\Storm\Database\Model $farParent + * @phpstan-property \Winter\Storm\Database\Model $parent + */ class HasOneThrough extends HasOneThroughBase { - use DefinedConstraints; + use Concerns\DefinedConstraints; /** * @var string The "name" of the relationship. diff --git a/src/Database/Relations/MorphMany.php b/src/Database/Relations/MorphMany.php index 10d4df8df..dca6564d9 100644 --- a/src/Database/Relations/MorphMany.php +++ b/src/Database/Relations/MorphMany.php @@ -6,10 +6,13 @@ use Illuminate\Database\Eloquent\Collection as CollectionBase; use Illuminate\Database\Eloquent\Relations\MorphMany as MorphManyBase; +/** + * @phpstan-property \Winter\Storm\Database\Model $parent + */ class MorphMany extends MorphManyBase { - use MorphOneOrMany; - use DefinedConstraints; + use Concerns\MorphOneOrMany; + use Concerns\DefinedConstraints; /** * Create a new has many relationship instance. diff --git a/src/Database/Relations/MorphOne.php b/src/Database/Relations/MorphOne.php index 77afefe7d..42bbabfaf 100644 --- a/src/Database/Relations/MorphOne.php +++ b/src/Database/Relations/MorphOne.php @@ -4,10 +4,13 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\MorphOne as MorphOneBase; +/** + * @phpstan-property \Winter\Storm\Database\Model $parent + */ class MorphOne extends MorphOneBase { - use MorphOneOrMany; - use DefinedConstraints; + use Concerns\MorphOneOrMany; + use Concerns\DefinedConstraints; /** * Create a new has many relationship instance. diff --git a/src/Database/Relations/MorphTo.php b/src/Database/Relations/MorphTo.php index 92e567eae..661cdc337 100644 --- a/src/Database/Relations/MorphTo.php +++ b/src/Database/Relations/MorphTo.php @@ -4,9 +4,13 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Relations\MorphTo as MorphToBase; +/** + * @phpstan-property \Winter\Storm\Database\Model $parent + */ class MorphTo extends MorphToBase { - use DefinedConstraints; + use Concerns\DeferOneOrMany; + use Concerns\DefinedConstraints; /** * @var string The "name" of the relationship. diff --git a/src/Database/Relations/MorphToMany.php b/src/Database/Relations/MorphToMany.php index b2771ca08..e8eb53b91 100644 --- a/src/Database/Relations/MorphToMany.php +++ b/src/Database/Relations/MorphToMany.php @@ -1,42 +1,24 @@ addDefinedConstraints(); } - /** - * Set the where clause for the relation query. - * - * @return $this - */ - protected function addWhereConstraints() - { - parent::addWhereConstraints(); - - $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); - - return $this; - } - - /** - * Set the constraints for an eager load of the relation. - * - * @param array $models - * @return void - */ - public function addEagerConstraints(array $models) - { - parent::addEagerConstraints($models); - - $this->query->where($this->table.'.'.$this->morphType, $this->morphClass); - } - - /** - * Create a new pivot attachment record. - * - * @param int $id - * @param bool $timed - * @return array - */ - protected function baseAttachRecord($id, $timed) - { - return Arr::add( - parent::baseAttachRecord($id, $timed), - $this->morphType, - $this->morphClass - ); - } - - /** - * Add the constraints for a relationship count query. - * - * @param \Illuminate\Database\Eloquent\Builder $query - * @param \Illuminate\Database\Eloquent\Builder $parentQuery - * @param array|mixed $columns - * @return \Illuminate\Database\Eloquent\Builder - */ - public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*']) - { - return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where( - $this->table.'.'.$this->morphType, - $this->morphClass - ); - } - /** * Create a new query builder for the pivot table. * @@ -172,24 +96,4 @@ public function newPivot(array $attributes = [], $exists = false) return $pivot; } - - /** - * Get the foreign key "type" name. - * - * @return string - */ - public function getMorphType() - { - return $this->morphType; - } - - /** - * Get the class name of the parent model. - * - * @return string - */ - public function getMorphClass() - { - return $this->morphClass; - } } diff --git a/src/Database/SortableScope.php b/src/Database/SortableScope.php index 48609c90f..08c477736 100644 --- a/src/Database/SortableScope.php +++ b/src/Database/SortableScope.php @@ -6,8 +6,6 @@ class SortableScope implements ScopeInterface { - protected $scopeApplied; - /** * Apply the scope to a given Eloquent query builder. * @@ -17,27 +15,9 @@ class SortableScope implements ScopeInterface */ public function apply(BuilderBase $builder, ModelBase $model) { - $this->scopeApplied = true; - - $builder->getQuery()->orderBy($model->getSortOrderColumn()); - } - - /** - * Extend the Eloquent query builder. - * - * @param \Illuminate\Database\Eloquent\Builder $builder - * @return void - */ - public function extend(BuilderBase $builder) - { - $builder->macro('orderBy', function ($builder, $column, $direction = 'asc') { - $builder - ->withoutGlobalScope($this) - ->getQuery() - ->orderBy($column, $direction) - ; - - return $builder; - }); + // Only apply the scope when no other explicit orders have been set + if (empty($builder->getQuery()->orders) && empty($builder->getQuery()->unionOrders)) { + $builder->orderBy($model->getSortOrderColumn()); + } } } diff --git a/src/Database/Traits/ArraySource.php b/src/Database/Traits/ArraySource.php new file mode 100644 index 000000000..2dc241354 --- /dev/null +++ b/src/Database/Traits/ArraySource.php @@ -0,0 +1,306 @@ + + * @author Winter CMS + */ +trait ArraySource +{ + /** + * Connection. to the SQLite datasource. + */ + protected static \Illuminate\Database\Connection $arraySourceConnection; + + /** + * Boots the ArraySource trait. + */ + public static function bootArraySource(): void + { + if (!in_array('sqlite', \PDO::getAvailableDrivers())) { + throw new ApplicationException('You must enable the SQLite PDO driver to use the ArraySource trait'); + } + + $instance = new static; + + static::arraySourceSetDbConnection( + (!$instance->arraySourceCanStoreDb()) ? ':memory:' : $instance->arraySourceGetDbPath() + ); + + if ($instance->arraySourceDbNeedsUpdate()) { + $instance->arraySourceCreateDb(); + } + } + + /** + * Gets the records stored with this model. + * + * This method may be overwritten to specify a custom data provider. It should always return an array of + * associative arrays, with column names for keys and a singular value for each column. + */ + public function getRecords(): array + { + if ($this->propertyExists('records')) { + if (!is_array($this->records)) { + throw new ApplicationException( + 'A model that uses the "ArraySource" trait must provide a "$records" property containing an array' + ); + } + + return $this->records; + } + + return []; + } + + /** + * @inheritDoc + */ + public static function resolveConnection($connection = null) + { + return static::$arraySourceConnection; + } + + /** + * Creates a connection to the temporary SQLite datasource. + * + * By default, this will create an in-memory database. + */ + protected static function arraySourceSetDbConnection(string $database): void + { + $config = [ + 'driver' => 'sqlite', + 'database' => $database, + ]; + + static::$arraySourceConnection = App::get(ConnectionFactory::class)->make($config); + } + + /** + * Creates the array source. + * + * This will create the temporary SQLite table and populate it with the given records. + */ + protected function arraySourceCreateDb(): void + { + if (File::exists($this->arraySourceGetDbPath())) { + File::delete($this->arraySourceGetDbPath()); + } + // Create SQLite file + File::put($this->arraySourceGetDbPath(), ''); + + $records = $this->getRecords(); + + $this->arraySourceCreateTable(); + + foreach (array_chunk($records, $this->arraySourceGetChunkSize()) as $inserts) { + static::insert($inserts); + } + } + + /** + * Creates the temporary SQLite table. + */ + protected function arraySourceCreateTable(): void + { + $builder = static::resolveConnection()->getSchemaBuilder(); + + try { + $builder->create($this->getTable(), function ($table) { + // Allow for overwriting schema types via the $recordSchema property + $schema = ($this->propertyExists('recordSchema')) + ? $this->recordSchema + : []; + $firstRecord = $this->getRecords()[0] ?? []; + + if (empty($schema) && empty($firstRecord)) { + throw new ApplicationException( + 'A model using the ArraySource trait must either provide "$records" or "$recordSchema" as an array.' + ); + } + + // Add incrementing field based on the primary key if the key is not found in the first record or schema + if ( + $this->incrementing + && !array_key_exists($this->primaryKey, $schema) + && !array_key_exists($this->primaryKey, $firstRecord) + ) { + $table->increments($this->primaryKey); + } + + if (!empty($firstRecord)) { + foreach ($firstRecord as $column => $value) { + $type = $this->arraySourceResolveDatatype($value); + + // Ensure the primary key is correctly created as an autoincremeting integer + if ($column === $this->primaryKey && $type === 'integer') { + $table->increments($this->primaryKey); + continue; + } + + $type = $schema[$column] ?? $type; + + $table->$type($column)->nullable(); + } + + // Create timestamp columns if they are not explicitly set in the first record + if ( + $this->usesTimestamps() + && ( + !in_array('created_at', array_keys($firstRecord)) + || !in_array('updated_at', array_keys($firstRecord)) + ) + ) { + $table->timestamps(); + } + } else { + foreach ($schema as $column => $type) { + // Ensure the primary key is correctly created as an autoincremeting integer + if ($column === $this->primaryKey && $type === 'integer') { + $table->increments($this->primaryKey); + continue; + } + + $table->$type($column)->nullable(); + } + + // Create timestamp columns if required + if ($this->usesTimestamps()) { + $table->timestamps(); + } + } + }); + } catch (QueryException $e) { + if (Str::contains($e->getMessage(), 'already exists (SQL: create table', true)) { + // Prevents race conditions on creating the table + return; + } + + throw $e; + } + } + + /** + * Determines the best column schema type for a given value + * + * @param mixed $value + */ + protected function arraySourceResolveDatatype($value): string + { + if (is_int($value)) { + return 'integer'; + } + + if (is_numeric($value)) { + return 'float'; + } + + if (is_string($value)) { + return 'string'; + } + + if (is_object($value) && $value instanceof \DateTimeInterface) { + return 'dateTime'; + } + + return 'string'; + } + + /** + * Determines if the temporary SQLite database for this model's array records will be stored. + */ + protected function arraySourceCanStoreDb(): bool + { + // A model may add a $cacheArray property which defines if this model will be cached or not + if ($this->propertyExists('cacheArray') && ((bool) $this->cacheArray) === false) { + return false; + } + + $sourceCacheDir = $this->arraySourceGetDbDir(); + + if ($sourceCacheDir === false) { + return false; + } + + if (!File::exists($sourceCacheDir)) { + if (!File::makeDirectory($sourceCacheDir, 0777, true)) { + return false; + } + } + + return File::isWritable($sourceCacheDir); + } + + /** + * Gets the directory where the array databases will be stored. + */ + protected function arraySourceGetDbDir(): string|false + { + $sourcePath = Config::get('database.arraySourcePath', storage_path('framework/cache/array-source/')); + + if ($sourcePath === false) { + return false; + } + + return PathResolver::resolve($sourcePath); + } + + /** + * Gets the path where the array database will be stored. + */ + protected function arraySourceGetDbPath(): string + { + $class = str_replace('\\', '', static::class); + return $this->arraySourceGetDbDir() . '/' . Str::kebab($class) . '.sqlite'; + } + + /** + * Determines if the stored array DB should be updated. + */ + protected function arraySourceDbNeedsUpdate(): bool + { + if (!$this->arraySourceCanStoreDb()) { + return true; + } + + if (!File::exists($this->arraySourceGetDbPath())) { + return true; + } + + $modelFile = (new ReflectionClass(static::class))->getFileName(); + + if (File::lastModified($this->arraySourceGetDbPath()) < File::lastModified($modelFile)) { + return true; + } + + return false; + } + + /** + * Sets the array chunk size when storing inserts. + * + * Sometimes, SQLite will complain if given too many records to insert at once, so we will split the records up + * into reasonable chunks and insert them in groups. + */ + protected function arraySourceGetChunkSize(): int + { + return 100; + } +} diff --git a/src/Database/Traits/Encryptable.php b/src/Database/Traits/Encryptable.php index 4218243f5..762eece0d 100644 --- a/src/Database/Traits/Encryptable.php +++ b/src/Database/Traits/Encryptable.php @@ -1,7 +1,7 @@ encrypter)) ? $this->encrypter : App::make('encrypter'); + return (!is_null($this->encrypterInstance)) ? $this->encrypterInstance : App::make('encrypter'); } /** @@ -119,6 +119,6 @@ public function getEncrypter() */ public function setEncrypter(\Illuminate\Contracts\Encryption\Encrypter $encrypter) { - $this->encrypter = $encrypter; + $this->encrypterInstance = $encrypter; } } diff --git a/src/Database/Traits/Hashable.php b/src/Database/Traits/Hashable.php index 6c4cd1ab4..6b697f563 100644 --- a/src/Database/Traits/Hashable.php +++ b/src/Database/Traits/Hashable.php @@ -1,7 +1,7 @@ getPurgeableAttributes(); } @@ -70,10 +69,9 @@ public function purgeAttributes($attributesToPurge = null) $cleanAttributes = array_diff_key($attributes, array_flip($purgeable)); $originalAttributes = array_diff_key($attributes, $cleanAttributes); - if (is_array($this->originalPurgeableValues)) { + if (count($this->originalPurgeableValues)) { $this->originalPurgeableValues = array_merge($this->originalPurgeableValues, $originalAttributes); - } - else { + } else { $this->originalPurgeableValues = $originalAttributes; } diff --git a/src/Database/Traits/Revisionable.php b/src/Database/Traits/Revisionable.php index 58a724095..9dcdc459f 100644 --- a/src/Database/Traits/Revisionable.php +++ b/src/Database/Traits/Revisionable.php @@ -1,9 +1,9 @@ getTable())->insert($toSave); + DB::table($revisionModel->getTable())->insert($toSave); $this->revisionableCleanUp(); } diff --git a/src/Database/Traits/Sortable.php b/src/Database/Traits/Sortable.php index 96499c99f..03ca6030d 100644 --- a/src/Database/Traits/Sortable.php +++ b/src/Database/Traits/Sortable.php @@ -75,6 +75,6 @@ public function setSortableOrder($itemIds, $itemOrders = null) */ public function getSortOrderColumn() { - return defined('static::SORT_ORDER') ? static::SORT_ORDER : 'sort_order'; + return defined('static::SORT_ORDER') ? constant('static::SORT_ORDER') : 'sort_order'; } } diff --git a/src/Database/Traits/Validation.php b/src/Database/Traits/Validation.php index a58d99e1a..94dca33d1 100644 --- a/src/Database/Traits/Validation.php +++ b/src/Database/Traits/Validation.php @@ -1,13 +1,13 @@ isValidScript($object); + $this->isValidScript($object, $file); - Eloquent::unguard(); + Model::unguard(); - if ($object instanceof Updates\Migration) { + if ($object instanceof Updates\Migration && method_exists($object, 'up')) { $object->up(); } - elseif ($object instanceof Updates\Seeder) { + elseif ($object instanceof Updates\Seeder && method_exists($object, 'run')) { $object->run(); } - Eloquent::reguard(); + Model::reguard(); return true; } @@ -52,15 +51,15 @@ public function packDown($file) return false; } - $this->isValidScript($object); + $this->isValidScript($object, $file); - Eloquent::unguard(); + Model::unguard(); - if ($object instanceof Updates\Migration) { + if ($object instanceof Updates\Migration && method_exists($object, 'down')) { $object->down(); } - Eloquent::reguard(); + Model::reguard(); return true; } @@ -68,16 +67,19 @@ public function packDown($file) /** * Resolve a migration instance from a file. * @param string $file - * @return object + * @return object|null */ public function resolve($file) { if (!File::isFile($file)) { - return; + return null; } - require_once $file; + $instance = require_once $file; + if (is_object($instance)) { + return $instance; + } if ($class = $this->getClassFromFile($file)) { return new $class; } @@ -86,7 +88,7 @@ public function resolve($file) /** * Checks if the object is a valid update script. */ - protected function isValidScript($object) + protected function isValidScript($object, $file) { if ($object instanceof Updates\Migration) { return true; @@ -96,15 +98,15 @@ protected function isValidScript($object) } throw new Exception(sprintf( - 'Database script [%s] must inherit Winter\Storm\Database\Updates\Migration or Winter\Storm\Database\Updates\Seeder classes', - get_class($object) + 'Database script [%s] must define a class that inherits the "Winter\Storm\Database\Updates\Migration" or "Winter\Storm\Database\Updates\Seeder" classes', + $file )); } /** * Extracts the namespace and class name from a file. * @param string $file - * @return string + * @return string|false */ public function getClassFromFile($file) { diff --git a/src/Events/Dispatcher.php b/src/Events/Dispatcher.php index 1d55250ca..788606829 100644 --- a/src/Events/Dispatcher.php +++ b/src/Events/Dispatcher.php @@ -1,10 +1,12 @@ listen($this->firstClosureParameterType($events), $events, $priority); + return; + } elseif ($events instanceof QueuedClosure) { + $this->listen($this->firstClosureParameterType($events->closure), $events->resolve(), $priority); + return; + } elseif ($listener instanceof QueuedClosure) { + $listener = $listener->resolve(); + } + foreach ((array) $events as $event) { if (Str::contains($event, '*')) { - $this->setupWildcardListen($event, $listener); + $this->setupWildcardListen($event, Serialization::wrapClosure($listener)); } else { $this->listeners[$event][$priority][] = $this->makeListener($listener); @@ -43,6 +61,20 @@ public function listen($events, $listener, $priority = 0) } } + /** + * Register an event listener with the dispatcher. + * + * @param \Closure|string|array $listener + * @param bool $wildcard + * @return \Closure + */ + public function makeListener($listener, $wildcard = false) + { + $listener = parent::makeListener($listener, $wildcard); + + return Serialization::wrapClosure($listener); + } + /** * Get the event that is currently firing. * @@ -71,7 +103,7 @@ public function until($event, $payload = []) * @param string|object $event * @param mixed $payload * @param bool $halt - * @return array|null + * @return array|mixed|null */ public function fire($event, $payload = [], $halt = false) { @@ -162,8 +194,8 @@ public function getListeners($eventName) /** * Sort the listeners for a given event by priority. * - * @param string $eventName - * @return array + * @param string $eventName + * @return void */ protected function sortListeners($eventName) { @@ -171,7 +203,7 @@ protected function sortListeners($eventName) // If listeners exist for the given event, we will sort them by the priority // so that we can call them in the correct order. We will cache off these - // sorted event listeners so we do not have to re-sort on every events. + // sorted event listeners so we do not have to re-sort on every event. if (isset($this->listeners[$eventName])) { krsort($this->listeners[$eventName]); diff --git a/src/Exception/ErrorHandler.php b/src/Exception/ErrorHandler.php index dfa6f5bfb..b429cf99e 100644 --- a/src/Exception/ErrorHandler.php +++ b/src/Exception/ErrorHandler.php @@ -1,10 +1,9 @@ setMask($proposedException); } @@ -76,10 +75,10 @@ public function handleException(Exception $proposedException) /** * Prepares a mask exception to be used when any exception fires. - * @param Exception $exception The mask exception. + * @param Throwable $exception The mask exception. * @return void */ - public static function applyMask(Exception $exception) + public static function applyMask(Throwable $exception) { if (static::$activeMask !== null) { array_push(static::$maskLayers, static::$activeMask); @@ -105,10 +104,10 @@ public static function removeMask() /** * Returns a more descriptive error message if application * debug mode is turned on. - * @param Exception $exception + * @param Throwable $exception * @return string */ - public static function getDetailedMessage($exception) + public static function getDetailedMessage(Throwable $exception) { /* * Application Exceptions never display a detailed error @@ -149,9 +148,9 @@ public function handleCustomError() /** * Displays the detailed system exception page. - * @return View Object containing the error page. + * @return \Illuminate\View\View|string Object containing the error page. */ - public function handleDetailedError($exception) + public function handleDetailedError(Throwable $exception) { return 'Error: ' . $exception->getMessage(); } diff --git a/src/Exception/ExceptionBase.php b/src/Exception/ExceptionBase.php index 418fd279c..157d1ba07 100644 --- a/src/Exception/ExceptionBase.php +++ b/src/Exception/ExceptionBase.php @@ -1,7 +1,8 @@ className === null) { $this->className = get_called_class(); @@ -106,10 +107,10 @@ public static function unmask() /** * If this exception acts as a mask, sets the face for the foreign exception. - * @param Exception $exception Face for the mask, the underlying exception. + * @param Throwable $exception Face for the mask, the underlying exception. * @return void */ - public function setMask(Exception $exception) + public function setMask(Throwable $exception) { $this->mask = $exception; $this->applyMask($exception); @@ -118,10 +119,10 @@ public function setMask(Exception $exception) /** * This method is used when applying the mask exception to the face exception. * It can be used as an override for child classes who may use different masking logic. - * @param Exception $exception Face exception being masked. + * @param Throwable $exception Face exception being masked. * @return void */ - public function applyMask(Exception $exception) + public function applyMask(Throwable $exception) { $this->file = $exception->getFile(); $this->message = $exception->getMessage(); @@ -132,7 +133,7 @@ public function applyMask(Exception $exception) /** * If this exception is acting as a mask, return the face exception. Otherwise return * this exception as the true one. - * @return Exception The underlying exception, or this exception if no mask is applied. + * @return Throwable The underlying exception, or this exception if no mask is applied. */ public function getTrueException() { @@ -146,7 +147,7 @@ public function getTrueException() /** * Generates information used for highlighting the area of code in context of the exception line number. * The highlighted block of code will be six (6) lines before and after the problem line number. - * @return array Highlight information as an array, the following keys are supplied: + * @return object Highlight information as an object, the following keys are supplied: * startLine - The starting line number, 6 lines before the error line. * endLine - The ending line number, 6 lines after the error line. * errorLine - The focused error line number. @@ -234,7 +235,7 @@ public function getCallStack() $args = null; if (isset($event['args']) && count($event['args'])) { - $args = $this->formatStackArguments($event['args'], false); + $args = $this->formatStackArguments($event['args']); } $result[] = (object)[ diff --git a/src/Extension/ExtendableTrait.php b/src/Extension/ExtendableTrait.php index a8e6be897..d8dbf998f 100644 --- a/src/Extension/ExtendableTrait.php +++ b/src/Extension/ExtendableTrait.php @@ -1,11 +1,12 @@ extensionData['dynamicMethods'][$dynamicName] = $method; + $this->extensionData['dynamicMethods'][$dynamicName] = Serialization::wrapClosure($method); } /** * Programmatically adds a property to the extendable class - * @param string $dynamicName - * @param string $value + * + * @param string $dynamicName The name of the property to add + * @param mixed $value The value of the property + * @return void */ public function addDynamicProperty($dynamicName, $value = null) { @@ -215,13 +216,16 @@ public function addDynamicProperty($dynamicName, $value = null) /** * Dynamically extend a class with a specified behavior - * @param string $extensionName + * @param string $extensionName * @return void */ public function extendClassWith($extensionName) { - if (!strlen($extensionName)) { - return $this; + if (empty($extensionName)) { + throw new Exception(sprintf( + 'You must provide an extension name to extend class %s with.', + get_class($this) + )); } $extensionName = $this->extensionNormalizeClassName($extensionName); @@ -365,8 +369,8 @@ protected function extendableIsAccessible($class, $propertyName) /** * Magic method for `__get()` - * @param string $name - * @return string + * @param string $name + * @return mixed|null */ public function extendableGet($name) { @@ -383,13 +387,15 @@ public function extendableGet($name) if ($parent !== false && method_exists($parent, '__get')) { return parent::__get($name); } + + return null; } /** * Magic method for `__set()` * @param string $name - * @param string $value - * @return string + * @param mixed $value + * @return void */ public function extendableSet($name, $value) { @@ -438,7 +444,7 @@ public function extendableCall($name, $params = null) $dynamicCallable = $this->extensionData['dynamicMethods'][$name]; if (is_callable($dynamicCallable)) { - return call_user_func_array($dynamicCallable, array_values($params)); + return call_user_func_array(Serialization::unwrapClosure($dynamicCallable), array_values($params)); } } diff --git a/src/Extension/ExtensionTrait.php b/src/Extension/ExtensionTrait.php index b587d983a..01f8f7143 100644 --- a/src/Extension/ExtensionTrait.php +++ b/src/Extension/ExtensionTrait.php @@ -1,5 +1,7 @@ getDefinitions($type); + return (new static)->getDefinitions($type); } /** * Returns a definition set from config or from the default sets. - * @param $type string - * @return array + * + * @throws Exception If the provided definition type does not exist. */ - public function getDefinitions($type) + public function getDefinitions(string $type): array { if (!method_exists($this, $type)) { throw new Exception(sprintf('No such definition set exists for "%s"', $type)); @@ -37,13 +42,13 @@ public function getDefinitions($type) } /** - * Determines if a path should be ignored, sourced from the ignoreFiles - * and ignorePatterns definitions. + * Determines if a path should be ignored based on the ignoreFiles and ignorePatterns definitions. + * + * Returns `true` if the path is ignored, `false` otherwise. + * * @todo Efficiency of this method can be improved. - * @param string $path Specifies a path to check. - * @return boolean Returns TRUE if the path is visible. */ - public static function isPathIgnored($path) + public static function isPathIgnored(string $path): bool { $ignoreNames = self::get('ignoreFiles'); $ignorePatterns = self::get('ignorePatterns'); @@ -63,10 +68,12 @@ public static function isPathIgnored($path) /** * Files that can be safely ignored. - * This list can be customized with config: - * - cms.fileDefinitions.ignoreFiles + * + * This list can be customized with the config: + * + * `cms.fileDefinitions.ignoreFiles` */ - protected function ignoreFiles() + protected function ignoreFiles(): array { return [ '.svn', @@ -78,10 +85,12 @@ protected function ignoreFiles() /** * File patterns that can be safely ignored. - * This list can be customized with config: - * - cms.fileDefinitions.ignorePatterns + * + * This list can be customized with the config: + * + * `cms.fileDefinitions.ignorePatterns` */ - protected function ignorePatterns() + protected function ignorePatterns(): array { return [ '^\..*' @@ -90,10 +99,12 @@ protected function ignorePatterns() /** * Extensions that are particularly benign. + * * This list can be customized with config: - * - cms.fileDefinitions.defaultExtensions + * + * `cms.fileDefinitions.defaultExtensions` */ - protected function defaultExtensions() + protected function defaultExtensions(): array { return [ 'jpg', @@ -142,10 +153,12 @@ protected function defaultExtensions() /** * Extensions seen as public assets. + * * This list can be customized with config: - * - cms.fileDefinitions.assetExtensions + * + * `cms.fileDefinitions.assetExtensions` */ - protected function assetExtensions() + protected function assetExtensions(): array { return [ 'jpg', @@ -171,10 +184,12 @@ protected function assetExtensions() /** * Extensions typically used as images. + * * This list can be customized with config: - * - cms.fileDefinitions.imageExtensions + * + * `cms.fileDefinitions.imageExtensions` */ - protected function imageExtensions() + protected function imageExtensions(): array { return [ 'jpg', @@ -188,10 +203,12 @@ protected function imageExtensions() /** * Extensions typically used as video files. + * * This list can be customized with config: - * - cms.fileDefinitions.videoExtensions + * + * `cms.fileDefinitions.videoExtensions` */ - protected function videoExtensions() + protected function videoExtensions(): array { return [ 'mp4', @@ -206,10 +223,12 @@ protected function videoExtensions() /** * Extensions typically used as audio files. + * * This list can be customized with config: - * - cms.fileDefinitions.audioExtensions + * + * `cms.fileDefinitions.audioExtensions` */ - protected function audioExtensions() + protected function audioExtensions(): array { return [ 'mp3', diff --git a/src/Filesystem/Filesystem.php b/src/Filesystem/Filesystem.php index c576ab487..ba8c3df9c 100644 --- a/src/Filesystem/Filesystem.php +++ b/src/Filesystem/Filesystem.php @@ -1,9 +1,9 @@ = 1073741824) { return number_format($bytes / 1073741824, 2) . ' GB'; @@ -87,12 +86,13 @@ public function sizeToString($bytes) } /** - * Returns a public file path from an absolute one - * eg: /home/mysite/public_html/welcome -> /welcome - * @param string $path Absolute path - * @return string + * Returns a public file path from an absolute path. + * + * Eg: `/home/mysite/public_html/welcome` -> `/welcome` + * + * Returns `null` if the path cannot be converted. */ - public function localToPublic($path) + public function localToPublic(string $path): ?string { $result = null; $publicPath = public_path(); @@ -124,12 +124,15 @@ public function localToPublic($path) } /** - * Returns true if the specified path is within the path of the application - * @param string $path The path to - * @param boolean $realpath Default true, uses realpath() to resolve the provided path before checking location. Set to false if you need to check if a potentially non-existent path would be within the application path - * @return boolean + * Determines if the given path is a local path. + * + * Returns `true` if the path is local, `false` otherwise. + * + * @param string $path The path to check + * @param boolean $realpath If `true` (default), the `realpath()` method will be used to resolve symlinks before checking if + * the path is local. Set to `false` if you are looking up non-existent paths. */ - public function isLocalPath($path, $realpath = true) + public function isLocalPath(string $path, bool $realpath = true): bool { $base = base_path(); @@ -141,34 +144,30 @@ public function isLocalPath($path, $realpath = true) } /** - * Returns true if the provided disk is using the "local" driver - * - * @param Illuminate\Filesystem\FilesystemAdapter $disk - * @return boolean + * Determines if the given disk is using the "local" driver. */ - public function isLocalDisk($disk) + public function isLocalDisk(\Illuminate\Filesystem\FilesystemAdapter $disk): bool { - return ($disk->getDriver()->getAdapter() instanceof \League\Flysystem\Adapter\Local); + return ($disk->getAdapter() instanceof \League\Flysystem\Local\LocalFilesystemAdapter); } /** - * Finds the path to a class - * @param mixed $className Class name or object - * @return string The file path + * Finds the path of a given class. + * + * Returns `false` if the path cannot be determined. + * + * @param string|object $className Class name or object */ - public function fromClass($className) + public function fromClass(string|object $className): string|false { $reflector = new ReflectionClass($className); return $reflector->getFileName(); } /** - * Determine if a file exists with case insensitivity - * supported for the file only. - * @param string $path - * @return mixed Sensitive path or false + * Determines if a file exists (ignoring the case for the filename only). */ - public function existsInsensitive($path) + public function existsInsensitive(string $path): string|false { if ($this->exists($path)) { return $path; @@ -191,52 +190,48 @@ public function existsInsensitive($path) } /** - * Normalizes the directory separator, often used by Win systems. - * @param string $path Path name - * @return string Normalized path + * Normalizes the directory separator, often used by Windows systems. */ - public function normalizePath($path) + public function normalizePath(string $path): string { return str_replace('\\', '/', $path); } /** - * Converts a path using path symbol. Returns the original path if - * no symbol is used and no default is specified. - * @param string $path - * @param mixed $default - * @return string + * Converts a path using path symbol. + * + * Returns the original path if no symbol is used, and no default is specified. */ - public function symbolizePath($path, $default = false) + public function symbolizePath(string $path, string|bool|null $default = null): string { - if (!$firstChar = $this->isPathSymbol($path)) { - return $default === false ? $path : $default; + if (!$this->isPathSymbol($path)) { + return (is_null($default)) ? $path : $default; } + $firstChar = substr($path, 0, 1); $_path = substr($path, 1); return $this->pathSymbols[$firstChar] . $_path; } /** - * Returns true if the path uses a symbol. - * @param string $path - * @return boolean + * Determines if the given path is using a path symbol. */ - public function isPathSymbol($path) + public function isPathSymbol(string $path): bool { - $firstChar = substr($path, 0, 1); - if (isset($this->pathSymbols[$firstChar])) { - return $firstChar; - } - - return false; + return array_key_exists(substr($path, 0, 1), $this->pathSymbols); } /** * Write the contents of a file. - * @param string $path - * @param string $contents - * @return int + * + * This method will also set the permissions based on the given chmod() mask in use. + * + * Returns the number of bytes written to the file, or `false` on failure. + * + * @param string $path + * @param string $contents + * @param bool|int $lock + * @return bool|int */ public function put($path, $contents, $lock = false) { @@ -247,8 +242,13 @@ public function put($path, $contents, $lock = false) /** * Copy a file to a new location. - * @param string $path - * @param string $target + * + * This method will also set the permissions based on the given chmod() mask in use. + * + * Returns `true` if successful, or `false` on failure. + * + * @param string $path + * @param string $target * @return bool */ public function copy($path, $target) @@ -260,26 +260,28 @@ public function copy($path, $target) /** * Create a directory. - * @param string $path - * @param int $mode - * @param bool $recursive - * @param bool $force + * + * @param string $path + * @param int $mode + * @param bool $recursive + * @param bool $force * @return bool */ public function makeDirectory($path, $mode = 0777, $recursive = false, $force = false) { - if ($mask = $this->getFolderPermissions()) { + $mask = $this->getFolderPermissions(); + if (!is_null($mask)) { $mode = $mask; } /* * Find the green leaves */ - if ($recursive && $mask) { + if ($recursive === true && !is_null($mask)) { $chmodPath = $path; while (true) { $basePath = dirname($chmodPath); - if ($chmodPath == $basePath) { + if ($chmodPath === $basePath) { break; } if ($this->isDirectory($basePath)) { @@ -287,8 +289,7 @@ public function makeDirectory($path, $mode = 0777, $recursive = false, $force = } $chmodPath = $basePath; } - } - else { + } else { $chmodPath = $path; } @@ -312,10 +313,11 @@ public function makeDirectory($path, $mode = 0777, $recursive = false, $force = } /** - * Modify file/folder permissions - * @param string $path - * @param octal $mask - * @return void + * Modify file/folder permissions. + * + * @param string $path + * @param int|float|null $mask + * @return bool */ public function chmod($path, $mask = null) { @@ -326,20 +328,16 @@ public function chmod($path, $mask = null) } if (!$mask) { - return; + return false; } return @chmod($path, $mask); } /** - * Modify file/folder permissions recursively - * @param string $path - * @param octal $fileMask - * @param octal $directoryMask - * @return void + * Modify file/folder permissions recursively in a given path. */ - public function chmodRecursive($path, $fileMask = null, $directoryMask = null) + public function chmodRecursive(string $path, int|float|null $fileMask = null, int|float|null $directoryMask = null): void { if (!$fileMask) { $fileMask = $this->getFilePermissions(); @@ -354,7 +352,8 @@ public function chmodRecursive($path, $fileMask = null, $directoryMask = null) } if (!$this->isDirectory($path)) { - return $this->chmod($path, $fileMask); + $this->chmod($path, $fileMask); + return; } $items = new FilesystemIterator($path, FilesystemIterator::SKIP_DOTS); @@ -372,9 +371,8 @@ public function chmodRecursive($path, $fileMask = null, $directoryMask = null) /** * Returns the default file permission mask to use. - * @return string Permission mask as octal (0777) or null */ - public function getFilePermissions() + public function getFilePermissions(): int|float|null { return $this->filePermissions ? octdec($this->filePermissions) @@ -383,9 +381,8 @@ public function getFilePermissions() /** * Returns the default folder permission mask to use. - * @return string Permission mask as octal (0777) or null */ - public function getFolderPermissions() + public function getFolderPermissions(): int|float|null { return $this->folderPermissions ? octdec($this->folderPermissions) @@ -394,11 +391,8 @@ public function getFolderPermissions() /** * Match filename against a pattern. - * @param string|array $fileName - * @param string $pattern - * @return bool */ - public function fileNameMatch($fileName, $pattern) + public function fileNameMatch(string $fileName, string $pattern): bool { if ($pattern === $fileName) { return true; @@ -410,11 +404,9 @@ public function fileNameMatch($fileName, $pattern) } /** - * Finds symlinks within the base path and provides a source => target array of symlinks. - * - * @return void + * Finds symlinks within the base path and populates the local symlinks property with an array of source => target symlinks. */ - protected function findSymlinks() + protected function findSymlinks(): void { $restrictBaseDir = Config::get('cms.restrictBaseDir', true); $deep = Config::get('develop.allowDeepSymlinks', false); diff --git a/src/Filesystem/FilesystemAdapter.php b/src/Filesystem/FilesystemAdapter.php deleted file mode 100644 index 30389a5ce..000000000 --- a/src/Filesystem/FilesystemAdapter.php +++ /dev/null @@ -1,105 +0,0 @@ -driver->getAdapter(); - - if ($adapter instanceof CachedAdapter) { - $adapter = $adapter->getAdapter(); - } - - if (method_exists($adapter, 'getUrl')) { - return $adapter->getUrl($path); - } elseif (method_exists($this->driver, 'getUrl')) { - return $this->driver->getUrl($path); - } elseif ($adapter instanceof AwsS3Adapter) { - return $this->getAwsUrl($adapter, $path); - } elseif ($adapter instanceof RackspaceAdapter) { - return $this->getRackspaceUrl($adapter, $path); - } elseif ($adapter instanceof Ftp) { - return $this->getFtpUrl($path); - } elseif ($adapter instanceof LocalAdapter) { - return $this->getLocalUrl($path); - } else { - throw new RuntimeException('This driver does not support retrieving URLs.'); - } - } - - /** - * Get the URL for the file at the given path. - * - * @param \League\Flysystem\Rackspace\RackspaceAdapter $adapter - * @param string $path - * @return string - */ - protected function getRackspaceUrl($adapter, $path) - { - return (string) $adapter->getContainer()->getObject($path)->getPublicUrl(); - } - - /** - * Get a temporary URL for the file at the given path. - * - * @param string $path - * @param \DateTimeInterface $expiration - * @param array $options - * @return string - * - * @throws \RuntimeException - */ - public function temporaryUrl($path, $expiration, array $options = []) - { - $adapter = $this->driver->getAdapter(); - - if ($adapter instanceof CachedAdapter) { - $adapter = $adapter->getAdapter(); - } - - if (method_exists($adapter, 'getTemporaryUrl')) { - return $adapter->getTemporaryUrl($path, $expiration, $options); - } elseif ($adapter instanceof AwsS3Adapter) { - return $this->getAwsTemporaryUrl($adapter, $path, $expiration, $options); - } elseif ($adapter instanceof RackspaceAdapter) { - return $this->getRackspaceTemporaryUrl($adapter, $path, $expiration, $options); - } else { - throw new RuntimeException('This driver does not support creating temporary URLs.'); - } - } - - /** - * Get a temporary URL for the file at the given path. - * - * @param \League\Flysystem\Rackspace\RackspaceAdapter $adapter - * @param string $path - * @param \DateTimeInterface $expiration - * @param array $options - * @return string - */ - public function getRackspaceTemporaryUrl($adapter, $path, $expiration, $options) - { - return $adapter->getContainer()->getObject($path)->getTemporaryUrl( - Carbon::now()->diffInSeconds($expiration), - $options['method'] ?? 'GET', - $options['forcePublicUrl'] ?? true - ); - } -} diff --git a/src/Filesystem/FilesystemManager.php b/src/Filesystem/FilesystemManager.php index e5d44b581..4d5d2ab82 100644 --- a/src/Filesystem/FilesystemManager.php +++ b/src/Filesystem/FilesystemManager.php @@ -1,23 +1,9 @@ $config['username'], 'apiKey' => $config['key'], - ], $config['options'] ?? []); - - $root = $config['root'] ?? null; - - return $this->adapt($this->createFlysystem( - new RackspaceAdapter($this->getRackspaceContainer($client, $config), $root), - $config - )); - } - - /** - * Get the Rackspace Cloud Files container. - * - * @param \OpenCloud\Rackspace $client - * @param array $config - * @return \OpenCloud\ObjectStore\Resource\Container - */ - protected function getRackspaceContainer(Rackspace $client, array $config) - { - $urlType = $config['url_type'] ?? null; + if (is_null($config)) { + $config = $this->getConfig($name); + } - $store = $client->objectStoreService('cloudFiles', $config['region'], $urlType); + // Default local drivers to public visibility for backwards compatibility + // see https://github.com/wintercms/winter/issues/503 + if ($name === 'local' && $config['driver'] === 'local' && empty($config['visibility'])) { + $config['visibility'] = 'public'; + } - return $store->getContainer($config['container']); + return parent::resolve($name, $config); } } diff --git a/src/Filesystem/FilesystemServiceProvider.php b/src/Filesystem/FilesystemServiceProvider.php index 9ebac6d14..40a10328f 100644 --- a/src/Filesystem/FilesystemServiceProvider.php +++ b/src/Filesystem/FilesystemServiceProvider.php @@ -1,21 +1,40 @@ extendFilesystemAdapter(); + $this->registerNativeFilesystem(); $this->registerFlysystem(); } + /** + * Extend Laravel's FilesystemAdapter class + * @return void + */ + protected function extendFilesystemAdapter() + { + FilesystemAdapter::macro('getPathPrefix', function () { + /** @phpstan-ignore-next-line */ + return $this->prefixer->prefixPath(''); + }); + FilesystemAdapter::macro('setPathPrefix', function (string $prefix) { + /** @phpstan-ignore-next-line */ + $this->prefixer = new PathPrefixer($prefix, $this->config['directory_separator'] ?? DIRECTORY_SEPARATOR); + }); + } + /** * Register the native filesystem implementation. * @return void @@ -28,8 +47,9 @@ protected function registerNativeFilesystem() $files->filePermissions = $config->get('cms.defaultMask.file', null); $files->folderPermissions = $config->get('cms.defaultMask.folder', null); $files->pathSymbols = [ - '$' => base_path() . $config->get('cms.pluginsDir', '/plugins'), '~' => base_path(), + '$' => base_path() . $config->get('cms.pluginsDir', '/plugins'), + '#' => base_path() . $config->get('cms.themesDir', '/themes'), ]; return $files; }); diff --git a/src/Filesystem/PathResolver.php b/src/Filesystem/PathResolver.php index 6d5d036b6..31377d26e 100644 --- a/src/Filesystem/PathResolver.php +++ b/src/Filesystem/PathResolver.php @@ -19,11 +19,8 @@ class PathResolver * and directories. * * Returns canonical path if it can be resolved, otherwise `false`. - * - * @param string $path The path to resolve - * @return string|bool */ - public static function resolve($path) + public static function resolve(string $path): string|bool { // Check if path is within any "open_basedir" restrictions if (!static::withinOpenBaseDir($path)) { @@ -100,12 +97,8 @@ public static function resolve($path) /** * Determines if the path is within the given directory. - * - * @param string $path - * @param string $directory - * @return bool */ - public static function within($path, $directory) + public static function within(string $path, string $directory): bool { $directory = static::resolve($directory); $path = static::resolve($path); @@ -115,12 +108,8 @@ public static function within($path, $directory) /** * Join two paths, making sure they use the correct directory separators. - * - * @param string $prefix - * @param string $path The path to add to the prefix. - * @return string */ - public static function join($prefix, $path = '') + public static function join(string $prefix, string $path = ''): string { $fullPath = rtrim(static::normalize($prefix, false) . '/' . static::normalize($path, false), '/'); @@ -133,11 +122,9 @@ public static function join($prefix, $path = '') * Converts any type of path (Unix or Windows) into a Unix-style path, so that we have a consistent format to work * with internally. All paths will be returned with no trailing path separator. * - * @param string $path - * @param bool $applyCwd If true, the current working directory will be appended if the path is relative. - * @return string + * If `$applyCwd` is true, the current working directory will be prepended if the path is relative. */ - protected static function normalize($path, $applyCwd = true) + protected static function normalize(string $path, bool $applyCwd = true): string { // Change directory separators to Unix-based $path = rtrim(str_replace('\\', '/', $path), '/'); @@ -159,11 +146,8 @@ protected static function normalize($path, $applyCwd = true) /** * Standardizes the path separators of a path back to the expected separator for the operating system. - * - * @param string $path - * @return string */ - public static function standardize($path) + public static function standardize(string $path): string { return str_replace('/', DIRECTORY_SEPARATOR, static::normalize($path, false)); } @@ -171,10 +155,9 @@ public static function standardize($path) /** * Resolves a symlink target. * - * @param mixed $path The symlink source's path. - * @return string|bool + * Returns the resolved symlink path, or `false` if it cannot be resolved. */ - protected static function resolveSymlink($symlink) + protected static function resolveSymlink($symlink): string|bool { // Check that the symlink is valid and the target exists $stat = linkinfo($symlink); @@ -205,11 +188,8 @@ protected static function resolveSymlink($symlink) /** * Checks if a given path is within "open_basedir" restrictions. - * - * @param string $path - * @return bool */ - protected static function withinOpenBaseDir($path) + protected static function withinOpenBaseDir(string $path): bool { $baseDirs = ini_get('open_basedir'); diff --git a/src/Filesystem/Zip.php b/src/Filesystem/Zip.php index ef9938550..d8ca7a62c 100644 --- a/src/Filesystem/Zip.php +++ b/src/Filesystem/Zip.php @@ -50,22 +50,28 @@ class Zip extends ZipArchive { /** - * @var string Folder prefix + * Folder prefix */ - protected $folderPrefix = ''; + protected string $folderPrefix = ''; /** - * Extract an existing zip file. - * @param string $source Path for the existing zip - * @param string $destination Path to extract the zip files - * @param array $options - * @return bool + * Lock down the constructor for this class. */ - public static function extract($source, $destination, $options = []) + final public function __construct() { - extract(array_merge([ - 'mask' => 0777 - ], $options)); + } + + /** + * Extracts an existing ZIP file. + * + * @param string $source Path to the ZIP file. + * @param string $destination Path to the destination directory. + * @param array $options Optional. An array of options. Only one option is currently supported: + * `mask`, which defines the permission mask to use when creating the destination folder. + */ + public static function extract(string $source, string $destination, array $options = []): bool + { + $mask = $options['mask'] ?? 0777; if (file_exists($destination) || mkdir($destination, $mask, true)) { $zip = new ZipArchive; @@ -80,24 +86,25 @@ public static function extract($source, $destination, $options = []) } /** - * Creates a new empty zip file. - * @param string $destination Path for the new zip - * @param mixed $source - * @param array $options - * @return self + * Creates a new empty Zip file, optionally populating it with given source files. + * + * Source can be a single path, an array of paths or a callback which allows you to manipulate + * the Zip file. + * + * @param string $destination Path to the destination ZIP file. + * @param string|callable|array|null $source Optional. Path to the source file(s) or a callback. + * @param array $options Optional. An array of options. Uses the same options as `Zip::add()`. */ - public static function make($destination, $source, $options = []) + public static function make(string $destination, string|callable|array|null $source = null, array $options = []): static { - $zip = new self; + $zip = new static; $zip->open($destination, ZIPARCHIVE::CREATE | ZipArchive::OVERWRITE); if (is_string($source)) { $zip->add($source, $options); - } - elseif (is_callable($source)) { + } elseif (is_callable($source)) { $source($zip); - } - elseif (is_array($source)) { + } elseif (is_array($source)) { foreach ($source as $_source) { $zip->add($_source, $options); } @@ -108,13 +115,22 @@ public static function make($destination, $source, $options = []) } /** - * Includes a source to the Zip - * @param mixed $source - * @param array $options - * @return self + * Adds a source file or directory to a Zip file. + * + * @param string $source Path to the source file or directory. + * @param array $options Optional. An array of options. Supports the following options: + * - `recursive`, which determines whether to add subdirectories and files recursively. + * Defaults to `true`. + * - `includeHidden`, which determines whether to add hidden files and directories. + * Defaults to `false`. + * - `baseDir`, which determines the base directory to use when adding files. + * - `baseglob`, which defines a glob pattern to match files and directories to add. */ - public function add($source, $options = []) + public function add(string $source, array $options = []): self { + $recursive = (bool) ($options['recursive'] ?? true); + $includeHidden = isset($options['includeHidden']) && $options['includeHidden'] === true; + /* * A directory has been supplied, convert it to a useful glob * @@ -124,23 +140,18 @@ public function add($source, $options = []) * - starts with '..' but has at least one character after it */ if (is_dir($source)) { - $includeHidden = isset($options['includeHidden']) && $options['includeHidden']; $wildcard = $includeHidden ? '{*,.[!.]*,..?*}' : '*'; $source = implode('/', [dirname($source), basename($source), $wildcard]); } - extract(array_merge([ - 'recursive' => true, - 'includeHidden' => false, - 'basedir' => dirname($source), - 'baseglob' => basename($source) - ], $options)); + $basedir = $options['basedir'] ?? dirname($source); + $baseglob = $options['baseglob'] ?? basename($source); if (is_file($source)) { $files = [$source]; + $folders = []; $recursive = false; - } - else { + } else { $files = glob($source, GLOB_BRACE); $folders = glob(dirname($source) . '/*', GLOB_ONLYDIR); } @@ -173,12 +184,11 @@ public function add($source, $options = []) } /** - * Creates a new folder inside the Zip and adds source files (optional) - * @param string $name Folder name - * @param mixed $source - * @return self + * Creates a new folder inside the Zip file, and optionally adds the given source files/folders to this folder. + * + * Source can be a single path, an array of paths or a callback which allows you to manipulate the Zip file. */ - public function folder($name, $source = null) + public function folder(string $name, string|callable|array|null $source = null): self { $prefix = $this->folderPrefix; $this->addEmptyDir($prefix . $name); @@ -190,11 +200,9 @@ public function folder($name, $source = null) if (is_string($source)) { $this->add($source); - } - elseif (is_callable($source)) { + } elseif (is_callable($source)) { $source($this); - } - elseif (is_array($source)) { + } elseif (is_array($source)) { foreach ($source as $_source) { $this->add($_source); } @@ -205,12 +213,11 @@ public function folder($name, $source = null) } /** - * Removes a file or folder from the zip collection. + * Removes file(s) or folder(s) from the Zip file. + * * Does not support wildcards. - * @param string $source - * @return self */ - public function remove($source) + public function remove(array|string $source): self { if (is_array($source)) { foreach ($source as $_source) { @@ -237,12 +244,9 @@ public function remove($source) } /** - * Removes a prefix from a path. - * @param string $prefix /var/sites/ - * @param string $path /var/sites/moo/cow/ - * @return string moo/cow/ + * Removes a prefix from a given path. */ - protected function removePathPrefix($prefix, $path) + protected function removePathPrefix(string $prefix, string $path): string { return (strpos($path, $prefix) === 0) ? substr($path, strlen($prefix)) diff --git a/src/Flash/FlashBag.php b/src/Flash/FlashBag.php index 71bf17ef2..61abb1f39 100644 --- a/src/Flash/FlashBag.php +++ b/src/Flash/FlashBag.php @@ -1,7 +1,7 @@ basePath, '/lang'); + return PathResolver::join($this->basePath, '/lang' . (!empty($path) ? "/$path" : '')); } /** @@ -151,6 +161,7 @@ protected function bindPathsInContainer() $this->instance('path.temp', $this->tempPath()); $this->instance('path.uploads', $this->uploadsPath()); $this->instance('path.media', $this->mediaPath()); + $this->instance('path.lang', $this->langPath()); } /** @@ -214,7 +225,7 @@ public function tempPath() /** * Set the temp path for the application. * - * @return string + * @return static */ public function setTempPath($path) { @@ -237,7 +248,7 @@ public function uploadsPath() /** * Set the uploads path for the application. * - * @return string + * @return static */ public function setUploadsPath($path) { @@ -260,7 +271,7 @@ public function mediaPath() /** * Set the media path for the application. * - * @return string + * @return static */ public function setMediaPath($path) { @@ -301,7 +312,7 @@ public function make($abstract, array $parameters = []) */ public function before($callback) { - return $this['router']->before($callback); + $this['router']->before($callback); } /** @@ -312,7 +323,7 @@ public function before($callback) */ public function after($callback) { - return $this['router']->after($callback); + $this['router']->after($callback); } /** @@ -334,7 +345,7 @@ public function error(Closure $callback) */ public function fatal(Closure $callback) { - $this->error(function (FatalErrorException $e) use ($callback) { + $this->error(function (FatalError $e) use ($callback) { return call_user_func($callback, $e); }); } @@ -380,12 +391,12 @@ public function setLocale($locale) /** * Register all of the configured providers. * - * @var bool $isRetry If true, this is a second attempt without the cached packages. + * @param bool $isRetry If true, this is a second attempt without the cached packages. * @return void */ public function registerConfiguredProviders($isRetry = false) { - $providers = Collection::make($this->config['app.providers']) + $providers = Collection::make($this->get('config')['app.providers']) ->partition(function ($provider) { return Str::startsWith($provider, 'Illuminate\\'); }); @@ -450,6 +461,7 @@ public function registerCoreContainerAliases() 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class], 'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class], 'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], + 'db.schema' => [\Illuminate\Database\Schema\Builder::class], 'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class], 'files' => [\Illuminate\Filesystem\Filesystem::class], 'filesystem' => [\Winter\Storm\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class], @@ -543,4 +555,31 @@ public function getCachedClassesPath() { return PathResolver::join($this->storagePath(), '/framework/classes.php'); } + + /** + * Get the application namespace. + * + * @return string + */ + public function getNamespace() + { + /** + * @TODO: Review calls to $app->getNamespace() that assume a single application namespace + * (Usually \App) instead of a collection of modules & plugins that all form the namespace. + * This is typically used for autoloading files and cleaning up output to remove extra + * unnecessary paths but those tasks should be handled completely differently in Winter CMS. + */ + return ''; + } + + /** + * This is a temporary fix for an issue with twig reflection. + * The full fix is here: https://github.com/twigphp/Twig/pull/3719 + * + * @TODO: Remove this after Twig PR 3719 is merged. + */ + public function __toString(): string + { + return get_called_class(); + } } diff --git a/src/Foundation/Bootstrap/LoadConfiguration.php b/src/Foundation/Bootstrap/LoadConfiguration.php index ae384fa15..3c26852f3 100644 --- a/src/Foundation/Bootstrap/LoadConfiguration.php +++ b/src/Foundation/Bootstrap/LoadConfiguration.php @@ -1,24 +1,22 @@ detectEnvironment(function () use ($app) { - return $this->getEnvironmentFromHost($app); + $app->detectEnvironment(function () { + return $this->getEnvironmentFromHost(); }); $app->instance('config', $config = new Repository($fileLoader, $app['env'])); @@ -28,15 +26,13 @@ public function bootstrap(Application $app) mb_internal_encoding('UTF-8'); // Fix for XDebug aborting threads > 100 nested - ini_set('xdebug.max_nesting_level', 1000); + ini_set('xdebug.max_nesting_level', '1000'); } /** * Returns the environment based on hostname. - * @param array $config - * @return void */ - protected function getEnvironmentFromHost(Application $app) + protected function getEnvironmentFromHost(): string { $config = $this->getEnvironmentConfiguration(); @@ -51,15 +47,14 @@ protected function getEnvironmentFromHost(Application $app) /** * Load the environment configuration. - * @return array */ - protected function getEnvironmentConfiguration() + protected function getEnvironmentConfiguration(): array { $config = []; $environment = env('APP_ENV'); - if ($environment && file_exists($configPath = base_path().'/config/'.$environment.'/environment.php')) { + if ($environment && file_exists($configPath = base_path() . '/config/' . $environment . '/environment.php')) { try { $config = require $configPath; } @@ -67,7 +62,7 @@ protected function getEnvironmentConfiguration() // } } - elseif (file_exists($configPath = base_path().'/config/environment.php')) { + elseif (file_exists($configPath = base_path() . '/config/environment.php')) { try { $config = require $configPath; } diff --git a/src/Foundation/Bootstrap/LoadEnvironmentVariables.php b/src/Foundation/Bootstrap/LoadEnvironmentVariables.php index 0300be2da..29474c833 100644 --- a/src/Foundation/Bootstrap/LoadEnvironmentVariables.php +++ b/src/Foundation/Bootstrap/LoadEnvironmentVariables.php @@ -1,11 +1,10 @@ checkForSpecificEnvironmentFile($app); - - try { - DotEnv::create($app->environmentPath(), $app->environmentFile())->load(); - } - catch (InvalidPathException $e) { - // - } - - $app->detectEnvironment(function () { - return env('APP_ENV', 'production'); - }); - } - - /** - * Detect if a custom environment file matching the APP_ENV exists. - * - * @param \Illuminate\Contracts\Foundation\Application $app - * @return void - */ - protected function checkForSpecificEnvironmentFile($app) - { - if ($app->runningInConsole() && ($input = new ArgvInput)->hasParameterOption('--env')) { - $this->setEnvironmentFilePath( - $app, - $app->environmentFile().'.'.$input->getParameterOption('--env') - ); - } - - if (!env('APP_ENV')) { + if ($app->configurationIsCached()) { return; } - $this->setEnvironmentFilePath( - $app, - $app->environmentFile().'.'.env('APP_ENV') - ); - } + // Force Laravel to do the work + parent::bootstrap($app); - /** - * Load a custom environment file. - * - * @param \Illuminate\Contracts\Foundation\Application $app - * @param string $file - * @return void - */ - protected function setEnvironmentFilePath($app, $file) - { - if (file_exists($app->environmentPath().'/'.$file)) { - $app->loadEnvironmentFrom($file); - } + // Ensure that the application will always have an environment name set + $app->detectEnvironment(function () { + return Env::get('APP_ENV', 'production'); + }); } } diff --git a/src/Foundation/Bootstrap/RegisterClassLoader.php b/src/Foundation/Bootstrap/RegisterClassLoader.php index 4df803f6b..21dd8c734 100644 --- a/src/Foundation/Bootstrap/RegisterClassLoader.php +++ b/src/Foundation/Bootstrap/RegisterClassLoader.php @@ -2,17 +2,14 @@ use Winter\Storm\Support\ClassLoader; use Winter\Storm\Filesystem\Filesystem; -use Illuminate\Contracts\Foundation\Application; +use Winter\Storm\Foundation\Application; class RegisterClassLoader { /** - * Register The Winter Auto Loader - * - * @param \Illuminate\Contracts\Foundation\Application $app - * @return void + * Register the Winter class loader service. */ - public function bootstrap(Application $app) + public function bootstrap(Application $app): void { $loader = new ClassLoader( new Filesystem, diff --git a/src/Foundation/Bootstrap/RegisterWinter.php b/src/Foundation/Bootstrap/RegisterWinter.php index da6719cc3..c9ff12699 100644 --- a/src/Foundation/Bootstrap/RegisterWinter.php +++ b/src/Foundation/Bootstrap/RegisterWinter.php @@ -1,16 +1,16 @@ get('cms.themesPathLocal')) { $app->setThemesPath($themesPath); } + + if ($tempPath = $app['config']->get('app.tempPath')) { + $app->setTempPath($tempPath); + } } } diff --git a/src/Foundation/Console/ClearCompiledCommand.php b/src/Foundation/Console/ClearCompiledCommand.php index 765b8ce9e..5348386e7 100644 --- a/src/Foundation/Console/ClearCompiledCommand.php +++ b/src/Foundation/Console/ClearCompiledCommand.php @@ -1,6 +1,5 @@ files = $files; + $env = EnvFile::open($this->laravel->environmentFilePath()); + $env->set('APP_KEY', $key); + $env->write(); } /** - * Execute the console command. + * Confirm before proceeding with the action. * - * @return void - */ - public function handle() - { - $key = $this->generateRandomKey(); - - if ($this->option('show')) { - return $this->line(''.$key.''); - } - - // Next, we will replace the application key in the config file so it is - // automatically setup for this developer. This key gets generated using a - // secure random byte generator and is later base64 encoded for storage. - if (!$this->setKeyInConfigFile($key)) { - return; - } - - $this->laravel['config']['app.key'] = $key; - - $this->info("Application key [$key] set successfully."); - } - - /** - * Set the application key in the config file. + * This method only asks for confirmation in production. * - * @param string $key + * @param string $warning + * @param \Closure|bool|null $callback * @return bool */ - protected function setKeyInConfigFile($key) + public function confirmToProceed($warning = 'Application In Production!', $callback = null) { - if (!$this->confirmToProceed()) { - return false; + if ($this->hasOption('force') && $this->option('force')) { + return true; } - $currentKey = $this->laravel['config']['app.key']; + $this->alert('An application key is already set!'); - list($path, $contents) = $this->getKeyFile(); + $confirmed = $this->confirm('Do you really wish to run this command?'); - $contents = str_replace($currentKey, $key, $contents); + if (!$confirmed) { + $this->comment('Command Canceled!'); - $this->files->put($path, $contents); + return false; + } return true; } - - /** - * Get the key file and contents. - * - * @return array - */ - protected function getKeyFile() - { - $env = $this->option('env') ? $this->option('env').'/' : ''; - - $contents = $this->files->get($path = $this->laravel['path.config']."/{$env}app.php"); - - return [$path, $contents]; - } } diff --git a/src/Foundation/Exception/Handler.php b/src/Foundation/Exception/Handler.php index e14052fef..5e351bdd5 100644 --- a/src/Foundation/Exception/Handler.php +++ b/src/Foundation/Exception/Handler.php @@ -1,22 +1,21 @@ > */ protected $dontReport = [ \Winter\Storm\Exception\AjaxException::class, @@ -34,14 +33,14 @@ class Handler extends ExceptionHandler protected $handlers = []; /** - * Report or log an exception. + * Report or log an throwable. * * This is a great spot to send exceptions to Sentry, Bugsnag, etc. * - * @param \Exception $exception + * @param \Throwable $throwable * @return void */ - public function report(Exception $exception) + public function report(Throwable $throwable) { /** * @event exception.beforeReport @@ -49,22 +48,22 @@ public function report(Exception $exception) * * Example usage (prevents the reporting of a given exception) * - * Event::listen('exception.report', function (\Exception $exception) { - * if ($exception instanceof \My\Custom\Exception) { + * Event::listen('exception.report', function (\Throwable $throwable) { + * if ($throwable instanceof \My\Custom\Exception) { * return false; * } * }); */ - if (app()->make('events')->fire('exception.beforeReport', [$exception], true) === false) { + if (app()->make('events')->fire('exception.beforeReport', [$throwable], true) === false) { return; } - if ($this->shouldntReport($exception)) { + if ($this->shouldntReport($throwable)) { return; } if (class_exists('Log')) { - Log::error($exception); + Log::error($throwable); } /** @@ -73,24 +72,24 @@ public function report(Exception $exception) * * Example usage (performs additional reporting on the exception) * - * Event::listen('exception.report', function (\Exception $exception) { - * app('sentry')->captureException($exception); + * Event::listen('exception.report', function (\Throwable $throwable) { + * app('sentry')->captureException($throwable); * }); */ - app()->make('events')->fire('exception.report', [$exception]); + app()->make('events')->fire('exception.report', [$throwable]); } /** * Render an exception into an HTTP response. * * @param \Illuminate\Http\Request $request - * @param \Exception $exception - * @return \Illuminate\Http\Response + * @param \Throwable $throwable + * @return \Symfony\Component\HttpFoundation\Response */ - public function render($request, Exception $exception) + public function render($request, Throwable $throwable) { - $statusCode = $this->getStatusCode($exception); - $response = $this->callCustomHandlers($exception); + $statusCode = $this->getStatusCode($throwable); + $response = $this->callCustomHandlers($throwable); if (!is_null($response)) { if ($response instanceof \Symfony\Component\HttpFoundation\Response) { @@ -100,25 +99,25 @@ public function render($request, Exception $exception) return Response::make($response, $statusCode); } - if ($event = app()->make('events')->fire('exception.beforeRender', [$exception, $statusCode, $request], true)) { + if ($event = app()->make('events')->fire('exception.beforeRender', [$throwable, $statusCode, $request], true)) { return Response::make($event, $statusCode); } - return parent::render($request, $exception); + return parent::render($request, $throwable); } /** * Checks if the exception implements the HttpExceptionInterface, or returns * as generic 500 error code for a server side error. - * @param \Exception $exception + * @param \Throwable $throwable * @return int */ - protected function getStatusCode($exception) + protected function getStatusCode($throwable) { - if ($exception instanceof HttpExceptionInterface) { - $code = $exception->getStatusCode(); + if ($throwable instanceof HttpExceptionInterface) { + $code = $throwable->getStatusCode(); } - elseif ($exception instanceof AjaxException) { + elseif ($throwable instanceof AjaxException) { $code = 406; } else { @@ -154,72 +153,85 @@ public function error(Closure $callback) } /** - * Handle the given exception. + * Handle the given throwable. * - * @param \Exception $exception + * @param \Throwable $throwable * @param bool $fromConsole - * @return void + * @return mixed|null */ - protected function callCustomHandlers($exception, $fromConsole = false) + protected function callCustomHandlers($throwable, $fromConsole = false) { foreach ($this->handlers as $handler) { - // If this exception handler does not handle the given exception, we will just - // go the next one. A handler may type-hint an exception that it handles so + // If this throwable handler does not handle the given throwable, we will just + // go the next one. A handler may type-hint an throwable that it handles so // we can have more granularity on the error handling for the developer. - if (!$this->handlesException($handler, $exception)) { + if (!$this->handlesThrowable($handler, $throwable)) { continue; } - $code = $this->getStatusCode($exception); + $code = $this->getStatusCode($throwable); // We will wrap this handler in a try / catch and avoid white screens of death - // if any exceptions are thrown from a handler itself. This way we will get + // if any throwables are thrown from a handler itself. This way we will get // at least some errors, and avoid errors with no data or not log writes. try { - $response = $handler($exception, $code, $fromConsole); - } - catch (Exception $e) { - $response = $this->convertExceptionToResponse($e); + $response = $handler($throwable, $code, $fromConsole); + } catch (Throwable $t) { + $response = $this->convertExceptionToResponse($t); } + // If this handler returns a "non-null" response, we will return it so it will // get sent back to the browsers. Once the handler returns a valid response // we will cease iterating through them and calling these other handlers. - if (isset($response) && ! is_null($response)) { + if (isset($response)) { return $response; } } } /** - * Determine if the given handler handles this exception. + * Determine if the given handler handles this throwable. * * @param \Closure $handler - * @param \Exception $exception + * @param \Throwable $throwable * @return bool */ - protected function handlesException(Closure $handler, $exception) + protected function handlesThrowable(Closure $handler, $throwable) { $reflection = new ReflectionFunction($handler); - return $reflection->getNumberOfParameters() == 0 || $this->hints($reflection, $exception); + return $reflection->getNumberOfParameters() == 0 || $this->hints($reflection, $throwable); } /** - * Determine if the given handler type hints the exception. + * Determine if the given handler type hints the throwable. * * @param \ReflectionFunction $reflection - * @param \Exception $exception + * @param \Throwable $throwable * @return bool */ - protected function hints(ReflectionFunction $reflection, $exception) + protected function hints(ReflectionFunction $reflection, $throwable) { $parameters = $reflection->getParameters(); $expected = $parameters[0]; - try { - return (new ReflectionClass($expected->getType()->getName())) - ->isInstance($exception); - } catch (Throwable $t) { - return false; + if ($expected->getType() instanceof \ReflectionNamedType) { + try { + return (new ReflectionClass($expected->getType()->getName())) + ->isInstance($throwable); + } catch (\Throwable $t) { + return false; + } + } elseif ($expected->getType() instanceof \ReflectionUnionType) { + foreach ($expected->getType()->getTypes() as $type) { + try { + return (new ReflectionClass($type->getName())) + ->isInstance($throwable); + } catch (\Throwable $t) { + return false; + } + } } + + return false; } } diff --git a/src/Foundation/Http/Kernel.php b/src/Foundation/Http/Kernel.php index c915c13e5..3d3e5bd24 100644 --- a/src/Foundation/Http/Kernel.php +++ b/src/Foundation/Http/Kernel.php @@ -7,7 +7,7 @@ class Kernel extends HttpKernel /** * The bootstrap classes for the application. * - * @var array + * @var string[] */ protected $bootstrappers = [ \Winter\Storm\Foundation\Bootstrap\RegisterClassLoader::class, @@ -71,7 +71,7 @@ class Kernel extends HttpKernel * * Forces the listed middleware to always be in the given order. * - * @var array + * @var string[] */ protected $middlewarePriority = [ \Illuminate\Session\Middleware\StartSession::class, diff --git a/src/Foundation/Http/Middleware/CheckForMaintenanceMode.php b/src/Foundation/Http/Middleware/CheckForMaintenanceMode.php index 851b21cc9..6a82797bf 100644 --- a/src/Foundation/Http/Middleware/CheckForMaintenanceMode.php +++ b/src/Foundation/Http/Middleware/CheckForMaintenanceMode.php @@ -2,11 +2,11 @@ namespace Winter\Storm\Foundation\Http\Middleware; -use Lang; -use View; use Closure; -use Response; use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware; +use Illuminate\Support\Facades\Lang; +use Illuminate\Support\Facades\View; +use Illuminate\Support\Facades\Response; class CheckForMaintenanceMode extends Middleware { diff --git a/src/Foundation/Http/Middleware/CheckForTrustedHost.php b/src/Foundation/Http/Middleware/CheckForTrustedHost.php index be181df88..128264104 100644 --- a/src/Foundation/Http/Middleware/CheckForTrustedHost.php +++ b/src/Foundation/Http/Middleware/CheckForTrustedHost.php @@ -1,6 +1,6 @@ allowProxies($request, [ + $this->allowProxies($request, [ $request->server->get('REMOTE_ADDR') ]); + return; } // Support comma-separated strings as well as arrays @@ -104,7 +105,7 @@ protected function setTrustedProxies(Request $request) : $proxies; if (is_array($proxies)) { - return $this->allowProxies($request, $proxies); + $this->allowProxies($request, $proxies); } } @@ -130,30 +131,40 @@ protected function getTrustedHeaders() $headers = $this->headers(); switch ($headers) { - case 'HEADER_X_FORWARDED_AWS_ELB': - case Request::HEADER_X_FORWARDED_AWS_ELB: - return Request::HEADER_X_FORWARDED_AWS_ELB; - break; case 'HEADER_FORWARDED': case Request::HEADER_FORWARDED: return Request::HEADER_FORWARDED; - break; - case 'HEADER_X_FORWARDED_ALL': - case Request::HEADER_X_FORWARDED_ALL: - return Request::HEADER_X_FORWARDED_ALL; - break; + + case 'HEADER_X_FORWARDED_FOR': + case Request::HEADER_X_FORWARDED_FOR: + return Request::HEADER_X_FORWARDED_FOR; + case 'HEADER_X_FORWARDED_HOST': case Request::HEADER_X_FORWARDED_HOST: return Request::HEADER_X_FORWARDED_HOST; - break; - case 'HEADER_X_FORWARDED_PORT': - case Request::HEADER_X_FORWARDED_PORT: - return Request::HEADER_X_FORWARDED_PORT; - break; + case 'HEADER_X_FORWARDED_PROTO': case Request::HEADER_X_FORWARDED_PROTO: return Request::HEADER_X_FORWARDED_PROTO; - break; + + case 'HEADER_X_FORWARDED_PORT': + case Request::HEADER_X_FORWARDED_PORT: + return Request::HEADER_X_FORWARDED_PORT; + + case 'HEADER_X_FORWARDED_PREFIX': + case Request::HEADER_X_FORWARDED_PREFIX: + return Request::HEADER_X_FORWARDED_PREFIX; + + case 'HEADER_X_FORWARDED_ALL': + return Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO; + + case 'HEADER_X_FORWARDED_AWS_ELB': + case Request::HEADER_X_FORWARDED_AWS_ELB: + return Request::HEADER_X_FORWARDED_AWS_ELB; + + case 'HEADER_X_FORWARDED_TRAEFIK': + case Request::HEADER_X_FORWARDED_TRAEFIK: + return Request::HEADER_X_FORWARDED_TRAEFIK; } return $headers; diff --git a/src/Foundation/Maker.php b/src/Foundation/Maker.php index d7ef8c8b8..9253a16b2 100644 --- a/src/Foundation/Maker.php +++ b/src/Foundation/Maker.php @@ -1,10 +1,12 @@ container = $container; + $this->app = $app; } - /** - * @param $abstract - * @param array $parameters - * - * @return mixed - */ - public function make($abstract, $parameters = []) + public function make($abstract, array $parameters = []) { return $this->build( $this->getBinding($abstract), @@ -43,12 +39,6 @@ public function make($abstract, $parameters = []) ); } - /** - * @param $abstract - * @param $concrete - * - * @return void - */ public function bind($abstract, Closure $concrete) { $this->bindings[$abstract] = $concrete; @@ -57,7 +47,7 @@ public function bind($abstract, Closure $concrete) protected function build($concrete, $parameters) { if ($concrete instanceof Closure) { - return $concrete($this->container, $parameters); + return $concrete($this->app, $parameters); } $reflector = new ReflectionClass($concrete); @@ -125,11 +115,9 @@ protected function getDependencies(array $parameters, array $primitives = []) if (array_key_exists($parameter->name, $primitives)) { $dependencies[] = $primitives[$parameter->name]; - } - elseif (is_null($dependency)) { + } elseif (is_null($dependency)) { $dependencies[] = $this->resolvePrimitive($parameter); - } - else { + } elseif ($dependency instanceof ReflectionUnionType === false) { $dependencies[] = $this->resolveClass($parameter); } } @@ -145,10 +133,12 @@ protected function getDependencies(array $parameters, array $primitives = []) */ protected function resolveClass(ReflectionParameter $parameter) { + /** @var ReflectionNamedType */ + $type = $parameter->getType(); + try { - return $this->getFromContainer($parameter->getType()->getName()); - } - catch (BindingResolutionException $e) { + return $this->getFromContainer($type->getName()); + } catch (BindingResolutionException $e) { if ($parameter->isOptional()) { return $parameter->getDefaultValue(); } @@ -156,19 +146,12 @@ protected function resolveClass(ReflectionParameter $parameter) } } - /** - * @param $abstract - * - * @return mixed - */ protected function getBinding($abstract) { return $this->isBound($abstract) ? $this->bindings[$abstract] : $abstract; } /** - * @param $abstract - * * @return bool */ protected function isBound($abstract) @@ -208,6 +191,6 @@ protected function unresolvablePrimitive(ReflectionParameter $parameter) */ protected function getFromContainer($abstract) { - return $this->container->make($abstract); + return $this->app->make($abstract); } } diff --git a/src/Foundation/ProviderRepository.php b/src/Foundation/ProviderRepository.php new file mode 100644 index 000000000..4bb1645b5 --- /dev/null +++ b/src/Foundation/ProviderRepository.php @@ -0,0 +1,48 @@ +loadManifest(); + + // First we will load the service manifest, which contains information on all + // service providers registered with the application and which services it + // provides. This is used to know which services are "deferred" loaders. + if ($this->shouldRecompile($manifest, $providers)) { + $manifest = $this->compileManifest($providers); + } + + // Next, we will register events to load the providers for each of the events + // that it has requested. This allows the service provider to defer itself + // while still getting automatically loaded when a certain event occurs. + foreach ($manifest['when'] as $provider => $events) { + $this->registerLoadEvents($provider, $events); + } + + // We will add the deferred services to the application so that they are able + // to be resolved if necessary during the registration process of the eagerly + // loaded providers. + $this->app->addDeferredServices($manifest['deferred']); + + // We will go ahead and register all of the eagerly loaded providers with the + // application so their services can be registered with the application as + // a provided service. + foreach ($manifest['eager'] as $provider) { + $this->app->register($provider); + } + } +} diff --git a/src/Foundation/Providers/ArtisanServiceProvider.php b/src/Foundation/Providers/ArtisanServiceProvider.php index 1fae0346e..3b6d348b4 100644 --- a/src/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Foundation/Providers/ArtisanServiceProvider.php @@ -12,32 +12,57 @@ class ArtisanServiceProvider extends ArtisanServiceProviderBase * @var array */ protected $commands = [ - 'CacheClear' => 'command.cache.clear', - 'CacheForget' => 'command.cache.forget', - 'ClearCompiled' => 'command.clear-compiled', - 'ConfigCache' => 'command.config.cache', - 'ConfigClear' => 'command.config.clear', - 'Down' => 'command.down', - 'Environment' => 'command.environment', - 'KeyGenerate' => 'command.key.generate', - 'Optimize' => 'command.optimize', - 'PackageDiscover' => 'command.package.discover', - 'QueueFailed' => 'command.queue.failed', - 'QueueFlush' => 'command.queue.flush', - 'QueueForget' => 'command.queue.forget', - 'QueueListen' => 'command.queue.listen', - 'QueueRestart' => 'command.queue.restart', - 'QueueRetry' => 'command.queue.retry', - 'QueueWork' => 'command.queue.work', - 'RouteCache' => 'command.route.cache', - 'RouteClear' => 'command.route.clear', - 'RouteList' => 'command.route.list', - 'ScheduleFinish' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, - 'ScheduleRun' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, - 'Seed' => 'command.seed', - 'StorageLink' => 'command.storage.link', - 'Up' => 'command.up', - 'ViewClear' => 'command.view.clear', + // Currently included in Winter + 'CacheClear' => \Illuminate\Cache\Console\ClearCommand::class, + 'CacheForget' => \Illuminate\Cache\Console\ForgetCommand::class, + 'ClearCompiled' => \Winter\Storm\Foundation\Console\ClearCompiledCommand::class, + 'ConfigCache' => \Illuminate\Foundation\Console\ConfigCacheCommand::class, + 'ConfigClear' => \Illuminate\Foundation\Console\ConfigClearCommand::class, + 'Down' => \Illuminate\Foundation\Console\DownCommand::class, + 'Environment' => \Illuminate\Foundation\Console\EnvironmentCommand::class, + 'KeyGenerate' => \Winter\Storm\Foundation\Console\KeyGenerateCommand::class, + 'Optimize' => \Illuminate\Foundation\Console\OptimizeCommand::class, + 'PackageDiscover' => \Illuminate\Foundation\Console\PackageDiscoverCommand::class, + 'QueueFailed' => \Illuminate\Queue\Console\ListFailedCommand::class, + 'QueueFlush' => \Illuminate\Queue\Console\FlushFailedCommand::class, + 'QueueForget' => \Illuminate\Queue\Console\ForgetFailedCommand::class, + 'QueueListen' => \Illuminate\Queue\Console\ListenCommand::class, + 'QueueMonitor' => \Illuminate\Queue\Console\MonitorCommand::class, + 'QueuePruneBatches' => \Illuminate\Queue\Console\PruneBatchesCommand::class, + 'QueuePruneFailedJobs' => \Illuminate\Queue\Console\PruneFailedJobsCommand::class, + 'QueueRestart' => \Illuminate\Queue\Console\RestartCommand::class, + 'QueueRetry' => \Illuminate\Queue\Console\RetryCommand::class, + 'QueueRetryBatch' => \Illuminate\Queue\Console\RetryBatchCommand::class, + 'QueueWork' => \Illuminate\Queue\Console\WorkCommand::class, + 'RouteCache' => \Illuminate\Foundation\Console\RouteCacheCommand::class, + 'RouteClear' => \Illuminate\Foundation\Console\RouteClearCommand::class, + 'RouteList' => \Illuminate\Foundation\Console\RouteListCommand::class, + 'ScheduleFinish' => \Illuminate\Console\Scheduling\ScheduleFinishCommand::class, + 'ScheduleRun' => \Illuminate\Console\Scheduling\ScheduleRunCommand::class, + 'Up' => \Illuminate\Foundation\Console\UpCommand::class, + 'ViewClear' => \Illuminate\Foundation\Console\ViewClearCommand::class, + + // Currently unsupported in Winter: + // @TODO: Assess for inclusion + // 'ClearResets' => ClearResetsCommand::class, + // 'Db' => DbCommand::class, + // 'DbPrune' => PruneCommand::class, + // 'DbWipe' => WipeCommand::class, + // 'EventCache' => EventCacheCommand::class, + // 'EventClear' => EventClearCommand::class, + // 'EventList' => EventListCommand::class, + // 'OptimizeClear' => OptimizeClearCommand::class, + // 'QueueClear' => QueueClearCommand::class, + // 'SchemaDump' => DumpCommand::class, + // 'ScheduleList' => ScheduleListCommand::class, + // 'ScheduleClearCache' => ScheduleClearCacheCommand::class, + // 'ScheduleTest' => ScheduleTestCommand::class, + // 'ScheduleWork' => ScheduleWorkCommand::class, + // 'ViewCache' => ViewCacheCommand::class, + + // Explicitly unsupported in Winter: + // 'Seed' => \Illuminate\Database\Console\Seeds\SeedCommand::class, // Use `winter:up` instead + // 'StorageLink' => \Illuminate\Foundation\Console\StorageLinkCommand::class, // Use `winter:mirror` instead. ]; /** @@ -46,8 +71,57 @@ class ArtisanServiceProvider extends ArtisanServiceProviderBase * @var array */ protected $devCommands = [ - 'Serve' => 'command.serve', - 'VendorPublish' => 'command.vendor.publish', + 'Serve' => \Illuminate\Foundation\Console\ServeCommand::class, + 'VendorPublish' => \Illuminate\Foundation\Console\VendorPublishCommand::class, + + // Currently unsupported in Winter + // @TODO: Assess for inclusion + // 'CacheTable' => CacheTableCommand::class, + // 'CastMake' => CastMakeCommand::class, + // 'ChannelMake' => ChannelMakeCommand::class, + // 'ComponentMake' => ComponentMakeCommand::class, + + + + // 'ControllerMake' => ControllerMakeCommand::class, + // 'EventGenerate' => EventGenerateCommand::class, + // 'EventMake' => EventMakeCommand::class, + // 'ExceptionMake' => ExceptionMakeCommand::class, + // 'FactoryMake' => FactoryMakeCommand::class, + // 'JobMake' => JobMakeCommand::class, + // 'ListenerMake' => ListenerMakeCommand::class, + // 'MailMake' => MailMakeCommand::class, + // 'MiddlewareMake' => MiddlewareMakeCommand::class, + + // 'ModelMake' => ModelMakeCommand::class, + + // MigrationServiceProvider + // 'Migrate' => MigrateCommand::class, + // 'MigrateFresh' => FreshCommand::class, + // 'MigrateInstall' => InstallCommand::class, + // 'MigrateRefresh' => RefreshCommand::class, + // 'MigrateReset' => ResetCommand::class, + // 'MigrateRollback' => RollbackCommand::class, + // 'MigrateStatus' => StatusCommand::class, + // 'MigrateMake' => MigrateMakeCommand::class, + + + // 'NotificationMake' => NotificationMakeCommand::class, + // 'NotificationTable' => NotificationTableCommand::class, + // 'ObserverMake' => ObserverMakeCommand::class, + // 'PolicyMake' => PolicyMakeCommand::class, + // 'ProviderMake' => ProviderMakeCommand::class, + // 'QueueFailedTable' => FailedTableCommand::class, + // 'QueueTable' => TableCommand::class, + // 'QueueBatchesTable' => BatchesTableCommand::class, + // 'RequestMake' => RequestMakeCommand::class, + // 'ResourceMake' => ResourceMakeCommand::class, + // 'RuleMake' => RuleMakeCommand::class, + // 'ScopeMake' => ScopeMakeCommand::class, + // 'SeederMake' => SeederMakeCommand::class, + // 'SessionTable' => SessionTableCommand::class, + // 'StubPublish' => StubPublishCommand::class, + // 'TestMake' => TestMakeCommand::class, ]; /** @@ -67,9 +141,7 @@ public function register() */ protected function registerKeyGenerateCommand() { - $this->app->singleton('command.key.generate', function ($app) { - return new KeyGenerateCommand($app['files']); - }); + $this->app->singleton(KeyGenerateCommand::class); } /** @@ -79,8 +151,6 @@ protected function registerKeyGenerateCommand() */ protected function registerClearCompiledCommand() { - $this->app->singleton('command.clear-compiled', function () { - return new ClearCompiledCommand; - }); + $this->app->singleton(ClearCompiledCommand::class); } } diff --git a/src/Foundation/Providers/ConsoleSupportServiceProvider.php b/src/Foundation/Providers/ConsoleSupportServiceProvider.php index f0c64c36c..87dcd0191 100644 --- a/src/Foundation/Providers/ConsoleSupportServiceProvider.php +++ b/src/Foundation/Providers/ConsoleSupportServiceProvider.php @@ -1,10 +1,11 @@ validateFileName(); @@ -360,10 +362,10 @@ public function insert(array $values) /** * Update a record in the datasource. * - * @param array $values - * @return int + * @param array $values The values to store in the model. + * @return int The filesize of the created model file. */ - public function update(array $values) + public function update(array $values = []): int { $this->validateFileName(); @@ -392,7 +394,7 @@ public function update(array $values) /** * Delete a record from the database. * - * @return int + * @return bool */ public function delete() { @@ -528,7 +530,7 @@ protected function validateFileNameExtension($fileName, $allowedExtensions) * Template directory and file names can contain only alphanumeric symbols, dashes and dots. * @param string $filePath Specifies a path to validate * @param integer $maxNesting Specifies the maximum allowed nesting level - * @return void + * @return bool */ protected function validateFileNamePath($filePath, $maxNesting = 2) { @@ -697,7 +699,7 @@ protected function isCacheBusted($result) /** * Get the cache object with tags assigned, if applicable. * - * @return \Illuminate\Cache\CacheManager + * @return \Illuminate\Contracts\Cache\Repository */ protected function getCache() { @@ -737,7 +739,7 @@ public function generateCacheKey() /** * Get the Closure callback used when caching queries. * - * @param string $fileName + * @param string|array $columns * @return \Closure */ protected function getCacheCallback($columns) @@ -749,8 +751,8 @@ protected function getCacheCallback($columns) /** * Initialize the cache data of each record. - * @param array $data - * @return array + * @param \Winter\Storm\Halcyon\Collection|array $data + * @return \Winter\Storm\Halcyon\Collection|array */ protected function processInitCacheData($data) { diff --git a/src/Halcyon/Datasource/Datasource.php b/src/Halcyon/Datasource/Datasource.php index 9f2323974..42bc1640b 100644 --- a/src/Halcyon/Datasource/Datasource.php +++ b/src/Halcyon/Datasource/Datasource.php @@ -1,56 +1,91 @@ postProcessor; } /** - * Force the deletion of a record against the datasource - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return void + * @inheritDoc + */ + abstract public function selectOne(string $dirName, string $fileName, string $extension): ?array; + + /** + * @inheritDoc + */ + abstract public function select(string $dirName, array $options = []): array; + + /** + * @inheritDoc + */ + abstract public function insert(string $dirName, string $fileName, string $extension, string $content): int; + + /** + * @inheritDoc + */ + abstract public function update(string $dirName, string $fileName, string $extension, string $content, ?string $oldFileName = null, ?string $oldExtension = null): int; + + /** + * @inheritDoc */ - public function forceDelete(string $dirName, string $fileName, string $extension) + abstract public function delete(string $dirName, string $fileName, string $extension): bool; + + /** + * @inheritDoc + */ + public function forceDelete(string $dirName, string $fileName, string $extension): bool { $this->forceDeleting = true; - $this->delete($dirName, $fileName, $extension); + $success = $this->delete($dirName, $fileName, $extension); $this->forceDeleting = false; + + return $success; } /** - * Generate a cache key unique to this datasource. + * @inheritDoc + */ + abstract public function lastModified(string $dirName, string $fileName, string $extension): ?int; + + /** + * @inheritDoc */ - public function makeCacheKey($name = '') + public function makeCacheKey(string $name = ''): string { - return crc32($name); + return hash('crc32b', $name); } + + /** + * @inheritDoc + */ + abstract public function getPathsCacheKey(): string; + + /** + * @inheritDoc + */ + abstract public function getAvailablePaths(): array; } diff --git a/src/Halcyon/Datasource/DatasourceInterface.php b/src/Halcyon/Datasource/DatasourceInterface.php index 1f328df0b..1c1fc25a9 100644 --- a/src/Halcyon/Datasource/DatasourceInterface.php +++ b/src/Halcyon/Datasource/DatasourceInterface.php @@ -2,99 +2,135 @@ interface DatasourceInterface { + /** + * Get the query post processor used by the connection. + */ + public function getPostProcessor(): \Winter\Storm\Halcyon\Processors\Processor; /** - * Returns a single template. + * Returns a single Halcyon model (template). * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return mixed + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. + * @return array|null An array of template data (`fileName`, `mtime` and `content`), or `null` if the model does + * not exist. */ - public function selectOne(string $dirName, string $fileName, string $extension); + public function selectOne(string $dirName, string $fileName, string $extension): ?array; /** - * Returns all templates. + * Returns all Halcyon models (templates) within a given directory. * - * @param string $dirName - * @param array $options - * @return array + * You can provide multiple options with the `$options` property, in order to filter the retrieved records: + * - `columns`: Only retrieve certain columns. Must be an array with any combination of `fileName`, `mtime` and + * `content`. + * - `extensions`: Defines the accepted extensions as an array. Eg: `['htm', 'md', 'twig']` + * - `fileMatch`: Defines a glob string to match filenames against. Eg: `'*gr[ae]y'` + * - `orders`: Not implemented + * - `limit`: Not implemented + * - `offset`: Not implemented + * + * @todo Implement support for `orders`, `limit` and `offset` options. + * @param string $dirName The directory in which the model is stored. + * @param array $options Defines the options for this query. + * @return array An array of models found, with the columns defined as per the `columns` parameter for `$options`. */ - public function select(string $dirName, array $options = []); + public function select(string $dirName, array $options = []): array; /** - * Creates a new template. + * Creates a new Halcyon model (template). * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @param array $content - * @return bool + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. + * @param string $content The content to store for the model. + * @return int The filesize of the created model. */ public function insert(string $dirName, string $fileName, string $extension, string $content); /** - * Updates an existing template. + * Updates an existing Halcyon model (template). * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @param array $content - * @param string $oldFileName Defaults to null - * @param string $oldExtension Defaults to null - * @return int + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. + * @param string $content The content to store for the model. + * @param string|null $oldFileName Used for renaming templates. If specified, this will delete the "old" path. + * @param string|null $oldExtension Used for renaming templates. If specified, this will delete the "old" path. + * @return int The filesize of the updated model. */ - public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null); + public function update( + string $dirName, + string $fileName, + string $extension, + string $content, + ?string $oldFileName = null, + ?string $oldExtension = null + ): int; /** - * Run a delete statement against the datasource. + * Runs a delete statement against the datasource. * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return bool + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. + * @return bool If the delete operation completed successfully. */ - public function delete(string $dirName, string $fileName, string $extension); + public function delete(string $dirName, string $fileName, string $extension): bool; /** - * Run a delete statement against the datasource, forcing the complete removal of the template + * Runs a delete statement against the datasource, forcing the complete removal of the model (template). * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return bool + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. + * @return bool If the delete operation completed successfully. */ - public function forceDelete(string $dirName, string $fileName, string $extension); + public function forceDelete(string $dirName, string $fileName, string $extension): bool; /** - * Return the last modified date of an object + * Returns the last modified date of a model (template). * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return int + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. + * @return int|null The last modified time as a timestamp, or `null` if the object doesn't exist. */ - public function lastModified(string $dirName, string $fileName, string $extension); + public function lastModified(string $dirName, string $fileName, string $extension): ?int; /** * Generate a cache key unique to this datasource. * - * @param string $name - * @return string + * @param string $name The name of the key. + * @return string The hashed key. */ - public function makeCacheKey($name = ''); + public function makeCacheKey(string $name = ''): string; /** - * Generate a paths cache key unique to this datasource + * Gets the prefix of the cache keys. + * + * This is based off a prefix including the base path for the model. * - * @return string + * @return string The cache key prefix. */ - public function getPathsCacheKey(); + public function getPathsCacheKey(): string; /** - * Get all available paths within this datastore + * Get all available paths within this datasource. + * + * This method returns an array, with all available paths as the key, and a boolean that represents whether the path + * can be handled or modified. + * + * Example: + * + * ```php + * [ + * 'path/to/file.md' => true, // (this path is available, and can be handled) + * 'path/to/file2.md' => false // (this path is available, but cannot be handled) + * ] + * ``` * - * @return array $paths ['path/to/file1.md' => true (path can be handled and exists), 'path/to/file2.md' => false (path can be handled but doesn't exist)] + * @return array An array of available paths alongside whether they can be handled. */ - public function getAvailablePaths(); + public function getAvailablePaths(): array; } diff --git a/src/Halcyon/Datasource/DbDatasource.php b/src/Halcyon/Datasource/DbDatasource.php index e4bc633b1..a91de342f 100644 --- a/src/Halcyon/Datasource/DbDatasource.php +++ b/src/Halcyon/Datasource/DbDatasource.php @@ -1,12 +1,12 @@ source = $source; - $this->table = $table; - $this->postProcessor = new Processor; } /** - * Get the base QueryBuilder object + * Get the base QueryBuilder object. */ - public function getBaseQuery() + public function getBaseQuery(): \Winter\Storm\Database\QueryBuilder { - return Db::table($this->table)->enableDuplicateCache(); + return DB::table($this->table)->enableDuplicateCache(); } /** - * Get the QueryBuilder object + * Get the QueryBuilder object. * - * @param bool $ignoreDeleted Flag to ignore deleted records, defaults to true - * @return QueryBuilder + * @param bool $ignoreDeleted Ignore deleted records. Defaults to `true`. */ - public function getQuery($ignoreDeleted = true) + public function getQuery(bool $ignoreDeleted = true): \Winter\Storm\Database\QueryBuilder { $query = $this->getBaseQuery(); @@ -90,27 +86,22 @@ public function getQuery($ignoreDeleted = true) } /** - * Helper to make file path. + * Helper method to combine the provided directory, filename and extension into a single path. * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return string + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. + * @return string The combined path. */ - protected function makeFilePath(string $dirName, string $fileName, string $extension) + protected function makeFilePath(string $dirName, string $fileName, string $extension): string { return $dirName . '/' . $fileName . '.' . $extension; } /** - * Returns a single template. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return mixed + * @inheritDoc */ - public function selectOne(string $dirName, string $fileName, string $extension) + public function selectOne(string $dirName, string $fileName, string $extension): ?array { $result = $this->getQuery()->where('path', $this->makeFilePath($dirName, $fileName, $extension))->first(); @@ -127,35 +118,28 @@ public function selectOne(string $dirName, string $fileName, string $extension) } /** - * Returns all templates. - * - * @param string $dirName - * @param array $options Array of options, [ - * 'columns' => ['fileName', 'mtime', 'content'], // Only return specific columns - * 'extensions' => ['htm', 'md', 'twig'], // Extensions to search for - * 'fileMatch' => '*gr[ae]y', // Shell matching pattern to match the filename against using the fnmatch function - * 'orders' => false // Not implemented - * 'limit' => false // Not implemented - * 'offset' => false // Not implemented - * ]; - * @return array + * @inheritDoc */ - public function select(string $dirName, array $options = []) + public function select(string $dirName, array $options = []): array { // Initialize result set $result = []; // Prepare query options - extract(array_merge([ + $queryOptions = array_merge([ 'columns' => null, // Only return specific columns (fileName, mtime, content) 'extensions' => null, // Match specified extensions 'fileMatch' => null, // Match the file name using fnmatch() 'orders' => null, // @todo 'limit' => null, // @todo 'offset' => null // @todo - ], $options)); + ], $options); + extract($queryOptions); - if ($columns === ['*'] || !is_array($columns)) { + if ( + isset($columns) + && ($columns === ['*'] || !is_array($columns)) + ) { $columns = null; } @@ -163,7 +147,7 @@ public function select(string $dirName, array $options = []) $query = $this->getQuery()->where('path', 'like', $dirName . '%'); // Apply the extensions filter - if (is_array($extensions) && !empty($extensions)) { + if (!empty($extensions) && is_array($extensions)) { $query->where(function ($query) use ($extensions) { // Get the first extension to query for $query->where('path', 'like', '%' . '.' . array_pop($extensions)); @@ -189,7 +173,7 @@ public function select(string $dirName, array $options = []) } // Apply the columns filter on the data returned - if (is_null($columns)) { + if (!isset($columns)) { $resultItem = [ 'fileName' => $fileName, 'content' => $item->content, @@ -221,15 +205,9 @@ public function select(string $dirName, array $options = []) } /** - * Creates a new template. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @param string $content - * @return bool + * @inheritDoc */ - public function insert(string $dirName, string $fileName, string $extension, string $content) + public function insert(string $dirName, string $fileName, string $extension, string $content): int { $path = $this->makeFilePath($dirName, $fileName, $extension); @@ -278,17 +256,9 @@ public function insert(string $dirName, string $fileName, string $extension, str } /** - * Updates an existing template. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @param string $content - * @param string $oldFileName Defaults to null - * @param string $oldExtension Defaults to null - * @return int + * @inheritDoc */ - public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null) + public function update(string $dirName, string $fileName, string $extension, string $content, ?string $oldFileName = null, ?string $oldExtension = null): int { $path = $this->makeFilePath($dirName, $fileName, $extension); @@ -338,14 +308,9 @@ public function update(string $dirName, string $fileName, string $extension, str } /** - * Run a delete statement against the datasource. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return bool + * @inheritDoc */ - public function delete(string $dirName, string $fileName, string $extension) + public function delete(string $dirName, string $fileName, string $extension): bool { try { // Get the existing record @@ -372,52 +337,31 @@ public function delete(string $dirName, string $fileName, string $extension) } /** - * Return the last modified date of an object - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return int + * @inheritDoc */ - public function lastModified(string $dirName, string $fileName, string $extension) + public function lastModified(string $dirName, string $fileName, string $extension): ?int { try { return Carbon::parse($this->getQuery() ->where('path', $this->makeFilePath($dirName, $fileName, $extension)) ->first()->updated_at)->timestamp; - } - catch (Exception $ex) { + } catch (Exception $ex) { return null; } } /** - * Generate a cache key unique to this datasource. - * - * @param string $name - * @return string + * @inheritDoc */ - public function makeCacheKey($name = '') - { - return crc32($this->source . $name); - } - - /** - * Generate a paths cache key unique to this datasource - * - * @return string - */ - public function getPathsCacheKey() + public function getPathsCacheKey(): string { return 'halcyon-datastore-db-' . $this->table . '-' . $this->source; } /** - * Get all available paths within this datastore - * - * @return array $paths ['path/to/file1.md' => true (path can be handled and exists), 'path/to/file2.md' => false (path can be handled but doesn't exist)] - */ - public function getAvailablePaths() + * @inheritDoc + **/ + public function getAvailablePaths(): array { /** * @event halcyon.datasource.db.beforeGetAvailablePaths diff --git a/src/Halcyon/Datasource/FileDatasource.php b/src/Halcyon/Datasource/FileDatasource.php index a6fabbdf6..b8cd59278 100644 --- a/src/Halcyon/Datasource/FileDatasource.php +++ b/src/Halcyon/Datasource/FileDatasource.php @@ -1,5 +1,8 @@ basePath = $basePath; - $this->files = $files; - $this->postProcessor = new Processor; } /** - * Returns a single template. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return mixed + * @inheritDoc */ - public function selectOne(string $dirName, string $fileName, string $extension) + public function selectOne(string $dirName, string $fileName, string $extension): ?array { try { $path = $this->makeFilePath($dirName, $fileName, $extension); @@ -72,36 +61,26 @@ public function selectOne(string $dirName, string $fileName, string $extension) 'content' => $this->files->get($path), 'mtime' => $this->files->lastModified($path) ]; - } - catch (Exception $ex) { + } catch (Exception $ex) { return null; } } /** - * Returns all templates. - * - * @param string $dirName - * @param array $options Array of options, [ - * 'columns' => ['fileName', 'mtime', 'content'], // Only return specific columns - * 'extensions' => ['htm', 'md', 'twig'], // Extensions to search for - * 'fileMatch' => '*gr[ae]y', // Shell matching pattern to match the filename against using the fnmatch function - * 'orders' => false // Not implemented - * 'limit' => false // Not implemented - * 'offset' => false // Not implemented - * ]; - * @return array + * @inheritDoc */ - public function select(string $dirName, array $options = []) + public function select(string $dirName, array $options = []): array { - extract(array_merge([ + // Prepare query options + $queryOptions = array_merge([ 'columns' => null, // Only return specific columns (fileName, mtime, content) 'extensions' => null, // Match specified extensions 'fileMatch' => null, // Match the file name using fnmatch() 'orders' => null, // @todo 'limit' => null, // @todo 'offset' => null // @todo - ], $options)); + ], $options); + extract($queryOptions); $result = []; $dirPath = $this->makeDirectoryPath($dirName); @@ -110,11 +89,12 @@ public function select(string $dirName, array $options = []) return $result; } - if ($columns === ['*'] || !is_array($columns)) { - $columns = null; - } - else { - $columns = array_flip($columns); + if (isset($columns)) { + if ($columns === ['*'] || !is_array($columns)) { + $columns = null; + } else { + $columns = array_flip($columns); + } } $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dirPath)); @@ -131,7 +111,7 @@ public function select(string $dirName, array $options = []) * Filter by extension */ $fileExt = $it->getExtension(); - if ($extensions !== null && !in_array($fileExt, $extensions)) { + if (isset($extensions) && !in_array($fileExt, $extensions)) { $it->next(); continue; } @@ -144,7 +124,7 @@ public function select(string $dirName, array $options = []) /* * Filter by file name match */ - if ($fileMatch !== null && !fnmatch($fileMatch, $fileName)) { + if (isset($fileMatch) && !fnmatch($fileMatch, $fileName)) { $it->next(); continue; } @@ -155,11 +135,11 @@ public function select(string $dirName, array $options = []) $item['fileName'] = $fileName; - if (!$columns || array_key_exists('content', $columns)) { + if (!isset($columns) || array_key_exists('content', $columns)) { $item['content'] = $this->files->get($path); } - if (!$columns || array_key_exists('mtime', $columns)) { + if (!isset($columns) || array_key_exists('mtime', $columns)) { $item['mtime'] = $this->files->lastModified($path); } @@ -172,15 +152,9 @@ public function select(string $dirName, array $options = []) } /** - * Creates a new template. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @param string $content - * @return bool + * @inheritDoc */ - public function insert(string $dirName, string $fileName, string $extension, string $content) + public function insert(string $dirName, string $fileName, string $extension, string $content): int { $this->validateDirectoryForSave($dirName, $fileName, $extension); @@ -192,24 +166,15 @@ public function insert(string $dirName, string $fileName, string $extension, str try { return $this->files->put($path, $content); - } - catch (Exception $ex) { + } catch (Exception $ex) { throw (new CreateFileException)->setInvalidPath($path); } } /** - * Updates an existing template. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @param string $content - * @param string $oldFileName Defaults to null - * @param string $oldExtension Defaults to null - * @return int + * @inheritDoc */ - public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null) + public function update(string $dirName, string $fileName, string $extension, string $content, ?string $oldFileName = null, ?string $oldExtension = null): int { $this->validateDirectoryForSave($dirName, $fileName, $extension); @@ -238,48 +203,35 @@ public function update(string $dirName, string $fileName, string $extension, str try { return $this->files->put($path, $content); - } - catch (Exception $ex) { + } catch (Exception $ex) { throw (new CreateFileException)->setInvalidPath($path); } } /** - * Run a delete statement against the datasource. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return bool + * @inheritDoc */ - public function delete(string $dirName, string $fileName, string $extension) + public function delete(string $dirName, string $fileName, string $extension): bool { $path = $this->makeFilePath($dirName, $fileName, $extension); try { return $this->files->delete($path); - } - catch (Exception $ex) { + } catch (Exception $ex) { throw (new DeleteFileException)->setInvalidPath($path); } } /** - * Run a delete statement against the datasource. - * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return int + * @inheritDoc */ - public function lastModified(string $dirName, string $fileName, string $extension) + public function lastModified(string $dirName, string $fileName, string $extension): ?int { try { $path = $this->makeFilePath($dirName, $fileName, $extension); return $this->files->lastModified($path); - } - catch (Exception $ex) { + } catch (Exception $ex) { return null; } } @@ -287,12 +239,11 @@ public function lastModified(string $dirName, string $fileName, string $extensio /** * Ensure the requested file can be created in the requested directory. * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return void + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. */ - protected function validateDirectoryForSave(string $dirName, string $fileName, string $extension) + protected function validateDirectoryForSave(string $dirName, string $fileName, string $extension): void { $path = $this->makeFilePath($dirName, $fileName, $extension); $dirPath = $this->makeDirectoryPath($dirName); @@ -330,7 +281,7 @@ protected function validateDirectoryForSave(string $dirName, string $fileName, s * @throws InvalidFileNameException If the path is outside of the basePath of the datasource * @return string */ - protected function makeDirectoryPath($dirName, $relativePath = '') + protected function makeDirectoryPath(string $dirName, string $relativePath = ''): string { $base = $this->basePath . '/' . $dirName; $path = !empty($relativePath) ? $base . '/' . $relativePath : $base; @@ -348,54 +299,38 @@ protected function makeDirectoryPath($dirName, $relativePath = '') } /** - * Helper to make file path. + * Helper method to make the full file path to the model. * - * @param string $dirName - * @param string $fileName - * @param string $extension - * @return string + * @param string $dirName The directory in which the model is stored. + * @param string $fileName The filename of the model. + * @param string $extension The file extension of the model. + * @return string The full file path. */ - protected function makeFilePath(string $dirName, string $fileName, string $extension) + protected function makeFilePath(string $dirName, string $fileName, string $extension): string { return $this->makeDirectoryPath($dirName, $fileName . '.' . $extension); } - /** - * Generate a cache key unique to this datasource. - * - * @param string $name - * @return string - */ - public function makeCacheKey($name = '') - { - return crc32($this->basePath . $name); - } - /** * Returns the base path for this datasource. - * @return string */ - public function getBasePath() + public function getBasePath(): string { return $this->basePath; } /** - * Generate a paths cache key unique to this datasource - * - * @return string + * @inheritDoc */ - public function getPathsCacheKey() + public function getPathsCacheKey(): string { return 'halcyon-datastore-file-' . $this->basePath; } /** - * Get all available paths within this datastore - * - * @return array $paths ['path/to/file1.md' => true (path can be handled and exists), 'path/to/file2.md' => false (path can be handled but doesn't exist)] + * @inheritDoc */ - public function getAvailablePaths() + public function getAvailablePaths(): array { $pathsCache = []; $it = (is_dir($this->basePath)) diff --git a/src/Halcyon/Datasource/Resolver.php b/src/Halcyon/Datasource/Resolver.php index 2d4dd11ed..8ce7d1f6b 100644 --- a/src/Halcyon/Datasource/Resolver.php +++ b/src/Halcyon/Datasource/Resolver.php @@ -1,5 +1,7 @@ getDefaultDatasource(); } + if (!array_key_exists($name, $this->datasources)) { + throw new MissingDatasourceException( + sprintf('The Halcyon datasource "%s" does not exist.', $name) + ); + } return $this->datasources[$name]; } /** - * Add a datasource to the resolver. - * - * @param string $name - * @param \Winter\Storm\Halcyon\Datasource\DatasourceInterface $datasource - * @return void + * @inheritDoc */ - public function addDatasource($name, DatasourceInterface $datasource) + public function addDatasource(string $name, DatasourceInterface $datasource): void { $this->datasources[$name] = $datasource; } /** - * Check if a datasource has been registered. - * - * @param string $name - * @return bool + * @inheritDoc */ - public function hasDatasource($name) + public function hasDatasource(string $name): bool { - return isset($this->datasources[$name]); + return array_key_exists($name, $this->datasources); } /** - * Get the default datasource name. - * - * @return string + * @inheritDoc */ - public function getDefaultDatasource() + public function getDefaultDatasource(): ?string { - return $this->default; + return $this->default ?? null; } /** - * Set the default datasource name. - * - * @param string $name - * @return void + * @inheritDoc */ - public function setDefaultDatasource($name) + public function setDefaultDatasource(string $name): void { $this->default = $name; } diff --git a/src/Halcyon/Datasource/ResolverInterface.php b/src/Halcyon/Datasource/ResolverInterface.php index 0f5a03730..96d17c054 100644 --- a/src/Halcyon/Datasource/ResolverInterface.php +++ b/src/Halcyon/Datasource/ResolverInterface.php @@ -1,28 +1,38 @@ many($key); @@ -44,7 +43,7 @@ public function get($key, $default = null) /** * Store an item in the cache. * - * @param string $key + * @param string|array $key * @param mixed $value * @param \DateTimeInterface|\DateInterval|int $seconds * @return bool @@ -92,12 +91,12 @@ public function decrement($key, $value = 1) * * @param string $key * @param mixed $value - * @return void + * @return bool */ public function forever($key, $value) { $this->putInMemoryCache($key, $value); - parent::forever($key, $value); + return parent::forever($key, $value); } /** @@ -128,8 +127,8 @@ public function flush() * Retrieve an item from the internal memory cache without trying the external driver. * Used in testing * - * @param $key - * @return mixed + * @param string $key + * @return mixed|null */ public function getFromMemoryCache($key) { @@ -140,8 +139,8 @@ public function getFromMemoryCache($key) * Puts an item in the memory cache, but not in the external cache. * Used in testing * - * @param $key - * @param $value + * @param string $key + * @param mixed $value */ public function putInMemoryCache($key, $value) { diff --git a/src/Halcyon/Model.php b/src/Halcyon/Model.php index d3cb0d696..427607cdd 100644 --- a/src/Halcyon/Model.php +++ b/src/Halcyon/Model.php @@ -15,19 +15,23 @@ /** * This is a base template object. Equivalent to a Model in ORM. * + * @property string|null $fileName Halcyon models generally provide a filename of the model being manipulated. + * @property int|null $mtime Halcyon models generally provide a timestamp of last modification. + * @method \Illuminate\Support\MessageBag|null errors() If the Validation trait is attached to the model, this method will provide the validation errors. + * * @author Alexey Bobkov, Samuel Georges */ -class Model extends Extendable implements ArrayAccess, Arrayable, Jsonable, JsonSerializable +class Model extends Extendable implements ModelInterface, ArrayAccess, Arrayable, Jsonable, JsonSerializable { use \Winter\Storm\Support\Traits\Emitter; /** - * @var string The data source for the model, a directory path. + * @var string|null The data source for the model, a directory path. */ protected $datasource; /** - * @var string The container name associated with the model, eg: pages. + * @var string|null The container name associated with the model, eg: pages. */ protected $dirName; @@ -105,21 +109,21 @@ class Model extends Extendable implements ArrayAccess, Arrayable, Jsonable, Json /** * The cache manager instance. * - * @var \Illuminate\Cache\CacheManager + * @var \Illuminate\Cache\CacheManager|null */ protected static $cache; /** * The datasource resolver instance. * - * @var \Winter\Storm\Halcyon\Datasource\ResolverInterface + * @var \Winter\Storm\Halcyon\Datasource\ResolverInterface|null */ protected static $resolver; /** * The event dispatcher instance. * - * @var \Illuminate\Contracts\Events\Dispatcher + * @var \Winter\Storm\Events\Dispatcher|null */ protected static $dispatcher; @@ -143,10 +147,7 @@ class Model extends Extendable implements ArrayAccess, Arrayable, Jsonable, Json protected static $booted = []; /** - * Create a new Halcyon model instance. - * - * @param array $attributes - * @return void + * @inheritDoc */ public function __construct(array $attributes = []) { @@ -406,7 +407,7 @@ public function isLoadedFromCache() /** * Returns true if the object was loaded from the cache. - * @return boolean + * @return void */ public function setLoadedFromCache($value) { @@ -556,7 +557,7 @@ public static function on($datasource = null) /** * Get all of the models from the datasource. * - * @return \Winter\Storm\Halcyon\Collection|static[] + * @return \Winter\Storm\Halcyon\Collection */ public static function all() { @@ -601,10 +602,8 @@ public function toJson($options = 0) /** * Convert the object into something JSON serializable. - * - * @return array */ - public function jsonSerialize() + public function jsonSerialize(): array { return $this->toArray(); } @@ -681,7 +680,8 @@ public function getAttribute($key) /** * @see Winter\Storm\Database\Model::getAttributeValue */ - if (($attr = $this->fireEvent('model.beforeGetAttribute', [$key], true)) !== null) { + $attr = $this->fireEvent('model.beforeGetAttribute', [$key], true); + if (!is_null($attr)) { return $attr; } @@ -697,7 +697,8 @@ public function getAttribute($key) /** * @see Winter\Storm\Database\Model::getAttributeValue */ - if (($_attr = $this->fireEvent('model.getAttribute', [$key, $value], true)) !== null) { + $_attr = $this->fireEvent('model.getAttribute', [$key, $value], true); + if (!is_null($_attr)) { return $_attr; } @@ -980,12 +981,12 @@ public function delete() */ protected function performDeleteOnModel() { - $this->newQuery()->delete($this->fileName); + $this->newQuery()->delete(); } /** * Create a new native event for handling beforeFetch(). - * @param Closure|string $callback + * @param \Closure|string $callback * @return void */ public static function fetching($callback) @@ -995,7 +996,7 @@ public static function fetching($callback) /** * Create a new native event for handling afterFetch(). - * @param Closure|string $callback + * @param \Closure|string $callback * @return void */ public static function fetched($callback) @@ -1213,7 +1214,7 @@ public function update(array $attributes = []) * @param array $options * @return bool */ - public function save(array $options = null) + public function save(?array $options = []) { return $this->saveInternal(['force' => false] + (array) $options); } @@ -1243,14 +1244,14 @@ public function saveInternal(array $options = []) } if ($this->exists) { - $saved = $this->performUpdate($query, $options); + $saved = $this->performUpdate($query); } else { - $saved = $this->performInsert($query, $options); + $saved = $this->performInsert($query); } if ($saved) { - $this->finishSave($options); + $this->finishSave(); } return $saved; @@ -1259,10 +1260,9 @@ public function saveInternal(array $options = []) /** * Finish processing on a successful save operation. * - * @param array $options * @return void */ - protected function finishSave(array $options) + protected function finishSave() { $this->fireModelEvent('saved', false); @@ -1274,11 +1274,10 @@ protected function finishSave(array $options) /** * Perform a model update operation. * - * @param Winter\Storm\Halcyon\Builder $query - * @param array $options + * @param \Winter\Storm\Halcyon\Builder $query * @return bool */ - protected function performUpdate(Builder $query, array $options = []) + protected function performUpdate(Builder $query) { $dirty = $this->getDirty(); @@ -1307,11 +1306,10 @@ protected function performUpdate(Builder $query, array $options = []) /** * Perform a model insert operation. * - * @param Winter\Storm\Halcyon\Builder $query - * @param array $options + * @param \Winter\Storm\Halcyon\Builder $query * @return bool */ - protected function performInsert(Builder $query, array $options = []) + protected function performInsert(Builder $query) { if ($this->fireModelEvent('creating') === false) { return false; @@ -1404,7 +1402,7 @@ public function getFileNameParts($fileName = null) /** * Get the datasource for the model. * - * @return \Winter\Storm\Halcyon\Datasource + * @return \Winter\Storm\Halcyon\Datasource\DatasourceInterface */ public function getDatasource() { @@ -1438,7 +1436,7 @@ public function setDatasource($name) * Resolve a datasource instance. * * @param string|null $datasource - * @return \Winter\Storm\Halcyon\Datasource + * @return \Winter\Storm\Halcyon\Datasource\DatasourceInterface */ public static function resolveDatasource($datasource = null) { @@ -1448,7 +1446,7 @@ public static function resolveDatasource($datasource = null) /** * Get the datasource resolver instance. * - * @return \Winter\Storm\Halcyon\DatasourceResolverInterface + * @return \Winter\Storm\Halcyon\Datasource\ResolverInterface */ public static function getDatasourceResolver() { @@ -1479,7 +1477,7 @@ public static function unsetDatasourceResolver() /** * Get the event dispatcher instance. * - * @return \Illuminate\Contracts\Events\Dispatcher + * @return \Winter\Storm\Events\Dispatcher */ public static function getEventDispatcher() { @@ -1489,7 +1487,7 @@ public static function getEventDispatcher() /** * Set the event dispatcher instance. * - * @param \Illuminate\Contracts\Events\Dispatcher $dispatcher + * @param \Winter\Storm\Events\Dispatcher $dispatcher * @return void */ public static function setEventDispatcher(Dispatcher $dispatcher) @@ -1510,7 +1508,7 @@ public static function unsetEventDispatcher() /** * Get the cache manager instance. * - * @return \Illuminate\Cache\CacheManager + * @return \Illuminate\Cache\CacheManager|null */ public static function getCacheManager() { @@ -1541,7 +1539,8 @@ public static function unsetCacheManager() /** * Initializes the object properties from the cached data. The extra data * set here becomes available as attributes set on the model after fetch. - * @param array $cached The cached data array. + * + * @param mixed $item */ public static function initCacheItem(&$item) { @@ -1553,9 +1552,13 @@ public static function initCacheItem(&$item) */ public static function flushDuplicateCache() { - if (MemoryCacheManager::isEnabled() && self::getCacheManager() !== null) { - self::getCacheManager()->driver()->flushInternalCache(); + if (!MemoryCacheManager::isEnabled() || is_null(self::getCacheManager())) { + return; } + + /** @var \Winter\Storm\Halcyon\MemoryRepository */ + $cacheDriver = self::getCacheManager()->driver(); + $cacheDriver->flushInternalCache(); } /** @@ -1625,7 +1628,7 @@ public function __set($key, $value) * @param mixed $offset * @return bool */ - public function offsetExists($offset) + public function offsetExists($offset): bool { return isset($this->$offset); } @@ -1636,7 +1639,7 @@ public function offsetExists($offset) * @param mixed $offset * @return mixed */ - public function offsetGet($offset) + public function offsetGet($offset): mixed { return $this->$offset; } @@ -1648,7 +1651,7 @@ public function offsetGet($offset) * @param mixed $value * @return void */ - public function offsetSet($offset, $value) + public function offsetSet($offset, $value): void { $this->$offset = $value; } @@ -1659,7 +1662,7 @@ public function offsetSet($offset, $value) * @param mixed $offset * @return void */ - public function offsetUnset($offset) + public function offsetUnset($offset): void { unset($this->$offset); } diff --git a/src/Halcyon/ModelInterface.php b/src/Halcyon/ModelInterface.php new file mode 100644 index 000000000..4da27073e --- /dev/null +++ b/src/Halcyon/ModelInterface.php @@ -0,0 +1,20 @@ + true, 'isCompoundObject' => true, - ], $options)); + ], $options); + extract($sectionOptions); - if (!$isCompoundObject) { + if (!isset($isCompoundObject) || $isCompoundObject === false) { return array_get($data, 'content', ''); } @@ -91,9 +92,9 @@ public static function render(array $data, array $options = []): string $settings = $iniParser->render($settings); // Prepare code section for saving - $code = trim(array_get($data, 'code', '')); + $code = trim(array_get($data, 'code', '') ?? ''); if ($code) { - if ($wrapCodeInPhpTags) { + if (isset($wrapCodeInPhpTags) && $wrapCodeInPhpTags === true) { $code = preg_replace('/^\<\?php/', '', $code); $code = preg_replace('/^\<\?/', '', $code); $code = preg_replace('/\?>$/', '', $code); @@ -189,9 +190,10 @@ public static function render(array $data, array $options = []): string */ public static function parse(string $content, array $options = []): array { - extract(array_merge([ - 'isCompoundObject' => true, - ], $options)); + $sectionOptions = array_merge([ + 'isCompoundObject' => true + ], $options); + extract($sectionOptions); $result = [ 'settings' => [], @@ -199,7 +201,7 @@ public static function parse(string $content, array $options = []): array 'markup' => null, ]; - if (!$isCompoundObject || !strlen($content)) { + if (!isset($isCompoundObject) || $isCompoundObject === false || !strlen($content)) { return $result; } @@ -222,7 +224,7 @@ public static function parse(string $content, array $options = []): array $result['markup'] = $sections[2]; } elseif ($count == 2) { - $result['settings'] = @$iniParser->parse($sections[0], true) + $result['settings'] = @$iniParser->parse($sections[0]) ?: [self::ERROR_INI => $sections[0]]; $result['markup'] = $sections[1]; @@ -267,9 +269,12 @@ public static function parseOffset(string $content): array } /** - * Returns the line number of a found instance of a section separator (==). + * Returns the line number of a found instance of CMS object section separator (==). + * @param string $content Object content + * @param int $instance Which instance to look for + * @return int|null The line number the instance was found. */ - private static function calculateLinePosition(string $content, int $instance = 1): int + protected static function calculateLinePosition(string $content, int $instance = 1): ?int { $count = 0; $lines = explode(PHP_EOL, $content); @@ -278,7 +283,7 @@ private static function calculateLinePosition(string $content, int $instance = 1 $count++; } - if ($count == $instance) { + if ($count === $instance) { return static::adjustLinePosition($content, $number); } } @@ -291,7 +296,7 @@ private static function calculateLinePosition(string $content, int $instance = 1 * after the separator (==). There can be an opening tag or white space in between * where the section really begins. */ - private static function adjustLinePosition(string $content, int $startLine = -1): int + protected static function adjustLinePosition(string $content, int $startLine = -1): int { // Account for the separator itself. $startLine++; diff --git a/src/Halcyon/Traits/Validation.php b/src/Halcyon/Traits/Validation.php index 687a32889..0c6a1950d 100644 --- a/src/Halcyon/Traits/Validation.php +++ b/src/Halcyon/Traits/Validation.php @@ -203,15 +203,8 @@ public function validate($rules = null, $customMessages = null, $attributeNames else { $this->validationErrors = $validator->messages(); - /* - * Flash input, if available - */ - if ( - ($input = Input::getFacadeRoot()) && - method_exists($input, 'hasSession') && - $input->hasSession() - ) { - $input->flash(); + if (Input::hasSession()) { + Input::flash(); } } } diff --git a/src/Html/BlockBuilder.php b/src/Html/BlockBuilder.php index d072bd25e..9c2d03192 100644 --- a/src/Html/BlockBuilder.php +++ b/src/Html/BlockBuilder.php @@ -9,42 +9,44 @@ */ class BlockBuilder { - protected $blockStack = []; - protected $blocks = []; + /** + * The block stack. + */ + protected array $blockStack = []; /** - * Helper for startBlock - * - * @param string $name Specifies the block name. - * @return void + * Registered block contents, keyed by block name. */ - public function put($name) + protected array $blocks = []; + + /** + * Helper method for the "startBlock" templating function. + */ + public function put(string $name): void { $this->startBlock($name); } /** - * Begins the layout block. + * Begins the layout block for a given block name. * * This method enables output buffering, so all output will be captured as a part of this block. - * - * @param string $name Specifies the block name. - * @return void */ - public function startBlock($name) + public function startBlock(string $name): void { array_push($this->blockStack, $name); ob_start(); } /** - * Helper for endBlock and also clears the output buffer. + * Helper method for the "endBlock" templating function. + * + * If `$append` is `true`, the new content should be appended to an existing block, as opposed to overwriting any + * previous content. * - * @param boolean $append Indicates that the new content should be appended to the existing block content. - * @return void * @throws \Exception if there are no items in the block stack */ - public function endPut($append = false) + public function endPut(bool $append = false): void { $this->endBlock($append); } @@ -54,11 +56,9 @@ public function endPut($append = false) * * This captures all buffered output as the block's content, and ends output buffering. * - * @param boolean $append Indicates that the new content should be appended to the existing block content. - * @return void * @throws \Exception if there are no items in the block stack */ - public function endBlock($append = false) + public function endBlock(bool $append = false): void { if (!count($this->blockStack)) { throw new Exception('Invalid block nesting'); @@ -75,30 +75,21 @@ public function endBlock($append = false) } /** - * Sets a content of the layout block. + * Sets a content of the layout block, overwriting any previous content for that block. * * Output buffering is not used for this method. - * - * @param string $name Specifies the block name. - * @param string $content Specifies the block content. - * @return void - * @throws \Exception if there are no items in the block stack */ - public function set($name, $content) + public function set(string $name, string $content): void { $this->blocks[$name] = $content; } /** - * Appends a content of the layout block. + * Appends content to a layout block. * * Output buffering is not used for this method. - * - * @param string $name Specifies the block name. - * @param string $content Specifies the block content. - * @return void */ - public function append($name, $content) + public function append(string $name, string $content): void { if (!isset($this->blocks[$name])) { $this->blocks[$name] = ''; @@ -108,13 +99,11 @@ public function append($name, $content) } /** - * Returns the layout block contents and deletes the block from memory. + * Returns the layout block contents of a given block name and deletes the block from memory. * - * @param string $name Specifies the block name. - * @param string $default Specifies a default block value to use if the block requested is not exists. - * @return string + * If the block does not exist, then the `$default` content will be returned instead. */ - public function placeholder($name, $default = null) + public function placeholder(string $name, string $default = null): ?string { $result = $this->get($name, $default); unset($this->blocks[$name]); @@ -127,16 +116,14 @@ public function placeholder($name, $default = null) } /** - * Returns the layout block contents but not deletes the block from memory. + * Returns the layout block contents of a given name, but does not delete it from memory. * - * @param string $name Specifies the block name. - * @param string $default Specifies a default block value to use if the block requested is not exists. - * @return string + * If the block does not exist, then the `$default` content will be returned instead. */ - public function get($name, $default = null) + public function get(string $name, string $default = null): ?string { if (!isset($this->blocks[$name])) { - return $default; + return $default; } return $this->blocks[$name]; @@ -144,10 +131,8 @@ public function get($name, $default = null) /** * Clears all the registered blocks. - * - * @return void */ - public function reset() + public function reset(): void { $this->blockStack = []; $this->blocks = []; @@ -155,10 +140,8 @@ public function reset() /** * Gets the block stack at this point. - * - * @return array */ - public function getBlockStack() + public function getBlockStack(): array { return $this->blockStack; } diff --git a/src/Html/FormBuilder.php b/src/Html/FormBuilder.php index c42816d07..a671a558a 100644 --- a/src/Html/FormBuilder.php +++ b/src/Html/FormBuilder.php @@ -14,88 +14,92 @@ class FormBuilder /** * The HTML builder instance. - * - * @var \Winter\Storm\Html\HtmlBuilder */ - protected $html; + protected \Winter\Storm\Html\HtmlBuilder $html; /** * The URL generator instance. - * - * @var \Illuminate\Routing\UrlGenerator $url */ - protected $url; + protected \Illuminate\Routing\UrlGenerator $url; /** * The CSRF token used by the form builder. - * - * @var string */ - protected $csrfToken; + protected ?string $csrfToken = null; /** * The session store implementation. - * - * @var \Illuminate\Session\Store */ - protected $session; + protected ?\Illuminate\Session\Store $session; /** * The current model instance for the form. - * - * @var mixed */ - protected $model; + protected object|array|null $model = null; /** * An array of label names we've created. - * - * @var array */ - protected $labels = []; + protected array $labels = []; /** * The reserved form open attributes. - * @var array */ - protected $reserved = ['method', 'url', 'route', 'action', 'files', 'request', 'model', 'sessionKey']; - - /** - * The reserved form open attributes. - * @var array - */ - protected $reservedAjax = ['request', 'success', 'error', 'complete', 'confirm', 'redirect', 'update', 'data', 'validate', 'flash']; + protected array $reserved = [ + 'method', + 'url', + 'route', + 'action', + 'files', + 'request', + 'model', + 'sessionKey' + ]; + + /** + * The reserved form AJAX attributes. + */ + protected array $reservedAjax = [ + 'request', + 'success', + 'error', + 'complete', + 'confirm', + 'redirect', + 'update', + 'data', + 'validate', + 'flash' + ]; /** * The form methods that should be spoofed, in uppercase. - * - * @var array */ - protected $spoofedMethods = ['DELETE', 'PATCH', 'PUT']; + protected array $spoofedMethods = [ + 'DELETE', + 'PATCH', + 'PUT' + ]; /** * The types of inputs to not fill values on by default. - * - * @var array */ - protected $skipValueTypes = ['file', 'password', 'checkbox', 'radio']; + protected array $skipValueTypes = [ + 'file', + 'password', + 'checkbox', + 'radio' + ]; /** * The session key used by the form builder. - * @var string */ - protected $sessionKey; + protected ?string $sessionKey = null; /** * Create a new form builder instance. - * - * @param \Winter\Storm\Html\HtmlBuilder $html - * @param \Illuminate\Routing\UrlGenerator $url - * @param string $csrfToken - * @param string $sessionKey - * @return void */ - public function __construct(HtmlBuilder $html, UrlGeneratorBase $url, $csrfToken, $sessionKey) + public function __construct(HtmlBuilder $html, UrlGeneratorBase $url, ?string $csrfToken = null, ?string $sessionKey = null) { $this->url = $url; $this->html = $html; @@ -105,10 +109,8 @@ public function __construct(HtmlBuilder $html, UrlGeneratorBase $url, $csrfToken /** * Open up a new HTML form and includes a session key. - * @param array $options - * @return string */ - public function open(array $options = []) + public function open(array $options = []): string { $method = strtoupper(array_get($options, 'method', 'post')); $request = array_get($options, 'request'); @@ -162,11 +164,8 @@ public function open(array $options = []) /** * Helper for opening a form used for an AJAX call. - * @param string $handler Request handler name, eg: onUpdate - * @param array $options - * @return string */ - public function ajax($handler, array $options = []) + public function ajax(string|array $handler, array $options = []): string { if (is_array($handler)) { $handler = implode('::', $handler); @@ -194,12 +193,8 @@ public function ajax($handler, array $options = []) /** * Create a new model based form builder. - * - * @param mixed $model - * @param array $options - * @return string */ - public function model($model, array $options = []) + public function model(object|array $model, array $options = []): string { $this->model = $model; @@ -208,21 +203,16 @@ public function model($model, array $options = []) /** * Set the model instance on the form builder. - * - * @param mixed $model - * @return void */ - public function setModel($model) + public function setModel(object|array|null $model): void { $this->model = $model; } /** * Close the current form. - * - * @return string */ - public function close() + public function close(): string { $this->labels = []; @@ -233,10 +223,8 @@ public function close() /** * Generate a hidden field with the current CSRF token. - * - * @return string */ - public function token() + public function token(): string { $token = !empty($this->csrfToken) ? $this->csrfToken @@ -247,13 +235,8 @@ public function token() /** * Create a form label element. - * - * @param string $name - * @param string $value - * @param array $options - * @return string */ - public function label($name, $value = null, $options = []) + public function label(string $name, string $value = '', array $options = []): string { $this->labels[] = $name; @@ -266,12 +249,8 @@ public function label($name, $value = null, $options = []) /** * Format the label value. - * - * @param string $name - * @param string|null $value - * @return string */ - protected function formatLabel($name, $value) + protected function formatLabel(string $name, string $value = ''): string { return $value ?: ucwords(str_replace('_', ' ', $name)); } @@ -279,109 +258,85 @@ protected function formatLabel($name, $value) /** * Create a form input field. * - * @param string $type - * @param string $name - * @param string $value - * @param array $options + * @param string $type + * @param string|null $name + * @param string|null $value + * @param array $options * @return string */ - public function input($type, $name, $value = null, $options = []) + public function input(string $type, ?string $name = null, ?string $value = null, array $options = []): string { if (!isset($options['name'])) { $options['name'] = $name; } - // We will get the appropriate value for the given field. We will look for the - // value in the session for the value in the old input data then we'll look - // in the model instance if one is set. Otherwise we will just use empty. - $id = $this->getIdAttribute($name, $options); + if (!empty($name)) { + // We will get the appropriate value for the given field. We will look for the + // value in the session for the value in the old input data then we'll look + // in the model instance if one is set. Otherwise we will just use empty. + $id = $this->getIdAttribute($name, $options); - if (!in_array($type, $this->skipValueTypes)) { - $value = $this->getValueAttribute($name, $value); - } + if (!in_array($type, $this->skipValueTypes)) { + $value = $this->getValueAttribute($name, $value); + } - // Once we have the type, value, and ID we can merge them into the rest of the - // attributes array so we can convert them into their HTML attribute format - // when creating the HTML element. Then, we will return the entire input. - $merge = compact('type', 'value', 'id'); + // Once we have the type, value, and ID we can merge them into the rest of the + // attributes array so we can convert them into their HTML attribute format + // when creating the HTML element. Then, we will return the entire input. + $merge = compact('type', 'value', 'id'); - $options = array_merge($options, $merge); + $options = array_filter(array_merge($options, $merge), function ($item) { + return !is_null($item); + }); + } - return 'html->attributes($options).'>'; + return 'html->attributes($options) . '>'; } /** * Create a text input field. - * - * @param string $name - * @param string $value - * @param array $options - * @return string */ - public function text($name, $value = null, $options = []) + public function text(string $name, ?string $value = null, array $options = []): string { return $this->input('text', $name, $value, $options); } /** * Create a password input field. - * - * @param string $name - * @param array $options - * @return string */ - public function password($name, $options = []) + public function password(string $name, array $options = []): string { return $this->input('password', $name, '', $options); } /** * Create a hidden input field. - * - * @param string $name - * @param string $value - * @param array $options - * @return string */ - public function hidden($name, $value = null, $options = []) + public function hidden(string $name, ?string $value = null, array $options = []): string { return $this->input('hidden', $name, $value, $options); } /** - * Create an e-mail input field. - * - * @param string $name - * @param string $value - * @param array $options - * @return string + * Create an email input field. */ - public function email($name, $value = null, $options = []) + public function email(string $name, ?string $value = null, array $options = []): string { return $this->input('email', $name, $value, $options); } /** - * Create a url input field. - * - * @param string $name - * @param string $value - * @param array $options - * @return string + * Create a URL input field. */ - public function url($name, $value = null, $options = []) + public function url(string $name, ?string $value = null, array $options = []): string { return $this->input('url', $name, $value, $options); } /** * Create a file input field. - * - * @param string $name - * @param array $options - * @return string */ - public function file($name, $options = []) + public function file(string $name, array $options = []): string { return $this->input('file', $name, null, $options); } @@ -392,13 +347,8 @@ public function file($name, $options = []) /** * Create a textarea input field. - * - * @param string $name - * @param string $value - * @param array $options - * @return string */ - public function textarea($name, $value = null, $options = []) + public function textarea(string $name, ?string $value = null, array $options = []): string { if (!isset($options['name'])) { $options['name'] = $name; @@ -425,11 +375,8 @@ public function textarea($name, $value = null, $options = []) /** * Set the text area size on the attributes. - * - * @param array $options - * @return array */ - protected function setTextAreaSize($options) + protected function setTextAreaSize(array $options): array { if (isset($options['size'])) { return $this->setQuickTextAreaSize($options); @@ -447,11 +394,8 @@ protected function setTextAreaSize($options) /** * Set the text area size using the quick "size" attribute. - * - * @param array $options - * @return array */ - protected function setQuickTextAreaSize($options) + protected function setQuickTextAreaSize(array $options): array { $segments = explode('x', $options['size']); @@ -464,13 +408,8 @@ protected function setQuickTextAreaSize($options) /** * Create a select box field with empty option support. - * @param string $name - * @param array $list - * @param string $selected - * @param array $options - * @return string */ - public function select($name, $list = [], $selected = null, $options = []) + public function select(string $name, array $list = [], string|array|null $selected = null, array $options = []): string { if (array_key_exists('emptyOption', $options)) { $list = ['' => $options['emptyOption']] + $list; @@ -508,15 +447,8 @@ public function select($name, $list = [], $selected = null, $options = []) /** * Create a select range field. - * - * @param string $name - * @param string $begin - * @param string $end - * @param string $selected - * @param array $options - * @return string */ - public function selectRange($name, $begin, $end, $selected = null, $options = []) + public function selectRange(string $name, string|int|float $begin, string|int|float $end, string|array|null $selected = null, array $options = []): string { $range = array_combine($range = range($begin, $end), $range); @@ -525,29 +457,19 @@ public function selectRange($name, $begin, $end, $selected = null, $options = [] /** * Create a select year field. - * - * @param string $name - * @param string $begin - * @param string $end - * @param string $selected - * @param array $options - * @return string */ - public function selectYear() + public function selectYear(string $name, int $begin = 1900, ?int $end = null, string|array|null $selected = null, array $options = []): string { - return call_user_func_array([$this, 'selectRange'], func_get_args()); + if (is_null($end)) { + $end = (int) date('Y'); + } + return $this->selectRange($name, $begin, $end, $selected, $options); } /** * Create a select month field. - * - * @param string $name - * @param string $selected - * @param array $options - * @param string $format - * @return string */ - public function selectMonth($name, $selected = null, $options = [], $format = '%B') + public function selectMonth(string $name, string|array|null $selected = null, array $options = [], $format = '%B'): string { $months = []; @@ -560,13 +482,8 @@ public function selectMonth($name, $selected = null, $options = [], $format = '% /** * Get the select option for the given value. - * - * @param string $display - * @param string $value - * @param string $selected - * @return string */ - public function getSelectOption($display, $value, $selected) + public function getSelectOption(string|array $display, string $value, string|array|null $selected = null): string { if (is_array($display)) { return $this->optionGroup($display, $value, $selected); @@ -577,13 +494,8 @@ public function getSelectOption($display, $value, $selected) /** * Create an option group form element. - * - * @param array $list - * @param string $label - * @param string $selected - * @return string */ - protected function optionGroup($list, $label, $selected) + protected function optionGroup(array $list, string $label, string|array|null $selected = null): string { $html = []; @@ -591,40 +503,38 @@ protected function optionGroup($list, $label, $selected) $html[] = $this->option($display, $value, $selected); } - return ''.implode('', $html).''; + return '' . implode('', $html) . ''; } /** * Create a select element option. - * - * @param string $display - * @param string $value - * @param string $selected - * @return string */ - protected function option($display, $value, $selected) + protected function option(string $display, string $value, string|array|null $selected = null): string { - $selected = $this->getSelectedValue($value, $selected); + $selectedAttr = $this->getSelectedValue($value, $selected); - $options = ['value' => e($value), 'selected' => $selected]; + $options = [ + 'value' => e($value), + 'selected' => $selectedAttr + ]; - return 'html->attributes($options).'>'.e($display).''; + return 'html->attributes($options) . '>' . e($display) . ''; } /** * Determine if the value is selected. - * - * @param string $value - * @param string $selected - * @return string */ - protected function getSelectedValue($value, $selected) + protected function getSelectedValue(string $value, string|array|null $selected): string|null { + if (is_null($selected)) { + return null; + } + if (is_array($selected)) { return in_array($value, $selected) ? 'selected' : null; } - return ((string) $value == (string) $selected) ? 'selected' : null; + return ((string) $value === (string) $selected) ? 'selected' : null; } // @@ -633,28 +543,16 @@ protected function getSelectedValue($value, $selected) /** * Create a checkbox input field. - * - * @param string $name - * @param mixed $value - * @param bool $checked - * @param array $options - * @return string */ - public function checkbox($name, $value = 1, $checked = null, $options = []) + public function checkbox(string $name, string $value = '1', bool $checked = false, array $options = []): string { return $this->checkable('checkbox', $name, $value, $checked, $options); } /** * Create a radio button input field. - * - * @param string $name - * @param mixed $value - * @param bool $checked - * @param array $options - * @return string */ - public function radio($name, $value = null, $checked = null, $options = []) + public function radio(string $name, ?string $value = null, bool $checked = false, array $options = []): string { if (is_null($value)) { $value = $name; @@ -665,15 +563,8 @@ public function radio($name, $value = null, $checked = null, $options = []) /** * Create a checkable input field. - * - * @param string $type - * @param string $name - * @param mixed $value - * @param bool $checked - * @param array $options - * @return string */ - protected function checkable($type, $name, $value, $checked, $options) + protected function checkable(string $type, string $name, string $value, bool $checked = false, array $options = []): string { $checked = $this->getCheckedState($type, $name, $value, $checked); @@ -947,18 +838,20 @@ public function getIdAttribute($name, $attributes) if (in_array($name, $this->labels)) { return $name; } + + return ''; } /** * Get the value that should be assigned to the field. * * @param string $name - * @param string $value - * @return string + * @param string|array $value + * @return string|array|null */ public function getValueAttribute($name, $value = null) { - if (is_null($name)) { + if (empty($name)) { return $value; } @@ -979,7 +872,7 @@ public function getValueAttribute($name, $value = null) * Get the model value that should be assigned to the field. * * @param string $name - * @return string + * @return string|array|null */ protected function getModelValueAttribute($name) { @@ -995,13 +888,15 @@ protected function getModelValueAttribute($name) * Get a value from the session's old input. * * @param string $name - * @return string + * @return string|array|null */ public function old($name) { if (isset($this->session)) { return $this->session->getOldInput($this->transformKey($name)); } + + return null; } /** @@ -1057,7 +952,7 @@ public function setSessionStore(Session $session) */ public function value($name, $value = null) { - if (is_null($name)) { + if (empty($name)) { return $value; } @@ -1104,7 +999,7 @@ public function sessionKey($sessionKey = null) /** * Returns the active session key, used fr deferred bindings. - * @return string + * @return string|null */ public function getSessionKey() { diff --git a/src/Html/Helper.php b/src/Html/Helper.php index 8df245c34..b521b9b15 100644 --- a/src/Html/Helper.php +++ b/src/Html/Helper.php @@ -11,7 +11,7 @@ class Helper * Converts a HTML array string to an identifier string. * HTML: user[location][city] * Result: user-location-city - * @param $string String to process + * @param string $string String to process * @return string */ public static function nameToId($string) @@ -23,7 +23,7 @@ public static function nameToId($string) * Converts a HTML named array string to a PHP array. Empty values are removed. * HTML: user[location][city] * PHP: ['user', 'location', 'city'] - * @param $string String to process + * @param string $string String to process * @return array */ public static function nameToArray($string) diff --git a/src/Html/HtmlBuilder.php b/src/Html/HtmlBuilder.php index 0f93f8808..6e5ef3307 100644 --- a/src/Html/HtmlBuilder.php +++ b/src/Html/HtmlBuilder.php @@ -108,7 +108,7 @@ public function image($url, $alt = null, $attributes = [], $secure = null) * Generate a HTML link. * * @param string $url - * @param string $title + * @param string|false|null $title * @param array $attributes * @param bool $secure * @return string @@ -281,7 +281,7 @@ protected function listing($type, $list, $attributes = []) * * @param mixed $key * @param string $type - * @param string $value + * @param string|array $value * @return string */ protected function listingElement($key, $type, $value) @@ -338,17 +338,17 @@ public function attributes($attributes) * Build a single attribute element. * * @param string $key - * @param string $value - * @return string|void + * @param string|array|null $value + * @return string|null */ - protected function attributeElement($key, $value) + protected function attributeElement($key, $value = null) { if (is_numeric($key)) { $key = $value; } if (is_null($value)) { - return; + return null; } if (is_array($value)) { @@ -395,7 +395,7 @@ public function obfuscate($value) /** * Removes HTML from a string - * @param $string String to strip HTML from + * @param string $string String to strip HTML from * @return string */ public static function strip($string) @@ -412,15 +412,11 @@ public static function strip($string) */ public static function limit($html, $maxLength = 100, $end = '...') { - $isUtf8 = true; $printedLength = 0; $position = 0; $tags = []; - $regex = $isUtf8 - ? '{]*>|&#?[a-zA-Z0-9]+;|[\x80-\xFF][\x80-\xBF]*}' - : '{]*>|&#?[a-zA-Z0-9]+;}'; - + $regex = '{]*>|&#?[a-zA-Z0-9]+;|[\x80-\xFF][\x80-\xBF]*}'; $result = ''; while ($printedLength < $maxLength && preg_match($regex, $html, $match, PREG_OFFSET_CAPTURE, $position)) { @@ -447,7 +443,7 @@ public static function limit($html, $maxLength = 100, $end = '...') else { $tagName = $match[1][0]; if ($tag[1] == '/') { - $openingTag = array_pop($tags); + array_pop($tags); $result .= $tag; } elseif ($tag[strlen($tag) - 2] == '/') { diff --git a/src/Mail/MailManager.php b/src/Mail/MailManager.php new file mode 100644 index 000000000..ade48d425 --- /dev/null +++ b/src/Mail/MailManager.php @@ -0,0 +1,74 @@ +app['events']->fire('mailer.beforeRegister', [$this]); + + return parent::mailer($name); + } + + /** + * Resolve the given mailer. + * + * @param string $name + * @return \Winter\Storm\Mail\Mailer + * + * @throws \InvalidArgumentException + */ + protected function resolve($name) + { + /** @var array|null */ + $config = $this->getConfig($name); + + if (is_null($config)) { + throw new InvalidArgumentException("Mailer [{$name}] is not defined."); + } + + // Once we have created the mailer instance we will set a container instance + // on the mailer. This allows us to resolve mailer classes via containers + // for maximum testability on said classes instead of passing Closures. + $mailer = new Mailer( + $name, + $this->app['view'], + $this->createSymfonyTransport($config), + $this->app['events'] + ); + + if ($this->app->bound('queue')) { + $mailer->setQueue($this->app['queue']); + } + + // Next we will set all of the global addresses on this mailer, which allows + // for easy unification of all "from" addresses as well as easy debugging + // of sent messages since these will be sent to a single email address. + foreach (['from', 'reply_to', 'to', 'return_path'] as $type) { + $this->setGlobalAddress($mailer, $config, $type); + } + + /* + * Extensibility + */ + $this->app['events']->fire('mailer.register', [$this, $mailer]); + + return $mailer; + } +} diff --git a/src/Mail/MailServiceProvider.php b/src/Mail/MailServiceProvider.php index bd00c307b..554323af0 100644 --- a/src/Mail/MailServiceProvider.php +++ b/src/Mail/MailServiceProvider.php @@ -5,54 +5,18 @@ class MailServiceProvider extends MailServiceProviderBase { /** - * Register the Illuminate mailer instance. Carbon copy of Illuminate method. + * Replace the Illuminate mailer instance with the Winter Mailer. + * * @return void */ protected function registerIlluminateMailer() { - $this->app->singleton('mailer', function ($app) { - /* - * Extensibility - */ - $this->app['events']->fire('mailer.beforeRegister', [$this]); - - $config = $app->make('config')->get('mail'); - - /* - * Winter mailer - */ - $mailer = new Mailer( - $app['view'], - $app['swift.mailer'], - $app['events'] - ); - - if ($app->bound('queue')) { - $mailer->setQueue($app['queue']); - } - - foreach (['from', 'reply_to', 'to'] as $type) { - $this->setGlobalAddress($mailer, $config, $type); - } - - /* - * Extensibility - */ - $this->app['events']->fire('mailer.register', [$this, $mailer]); - - return $mailer; + $this->app->singleton('mail.manager', function ($app) { + return new MailManager($app); }); - } - /** - * Register the Swift Transport instance. - * - * @return void - */ - protected function registerSwiftTransport() - { - $this->app->singleton('swift.transport', function ($app) { - return new TransportManager($app); + $this->app->bind('mailer', function ($app) { + return $app->make('mail.manager')->mailer(); }); } } diff --git a/src/Mail/Mailable.php b/src/Mail/Mailable.php index 69cb77c15..78722102e 100644 --- a/src/Mail/Mailable.php +++ b/src/Mail/Mailable.php @@ -1,6 +1,6 @@ $view]; + } + elseif (!array_key_exists('raw', $view)) { + $view['raw'] = true; + } + + return $this->send($view, [], $callback); + } + /** * Send a new message using a view. + * Overrides the Laravel defaults to provide the following functionality: + * - Events (global & local): + * - mailer.beforeSend + * - mailer.prepareSend + * - mailer.send + * - Custom addContent() behavior + * - Support for bypassing all addContent behavior when passing $view['raw' => true] * - * @param string|array $view - * @param array $data - * @param \Closure|string $callback - * @return mixed + * @param \Illuminate\Contracts\Mail\Mailable|string|array $view + * @param array $data + * @param \Closure|string|null $callback + * @return \Illuminate\Mail\SentMessage|null */ public function send($view, array $data = [], $callback = null) { @@ -51,31 +78,40 @@ public function send($view, array $data = [], $callback = null) ($this->fireEvent('mailer.beforeSend', [$view, $data, $callback], true) === false) || (Event::fire('mailer.beforeSend', [$view, $data, $callback], true) === false) ) { - return; + return null; } if ($view instanceof MailableContract) { return $this->sendMailable($view); } - /* - * Inherit logic from Illuminate\Mail\Mailer - */ + // First we need to parse the view, which could either be a string or an array + // containing both an HTML and plain text versions of the view which should + // be used when sending an e-mail. We will extract both of them out here. list($view, $plain, $raw) = $this->parseView($view); $data['message'] = $message = $this->createMessage(); + // Once we have retrieved the view content for the e-mail we will set the body + // of this message using the HTML type, which will provide a simple wrapper + // to creating view based emails that are able to receive arrays of data. if ($callback !== null) { call_user_func($callback, $message); } + // When $raw === true, attach the content directly to the + // message without any form of parsing or events being fired. + // @see https://github.com/wintercms/storm/commit/7fdc46cb6c2424436b1eb1cb1a66223785d7520f + // @see https://github.com/wintercms/storm/commit/aa1e96c5741f14900311daa2cad3826aaf97f6c8 if (is_bool($raw) && $raw === true) { $this->addContentRaw($message, $view, $plain); - } - else { + } else { $this->addContent($message, $view, $plain, $raw, $data); } + // If a global "to" address has been set, we will set that address on the mail + // message. This is primarily useful during local development in which each + // message should be delivered into a single mail address for inspection. if (isset($this->to['address'])) { $this->setGlobalToAndRemoveCcAndBcc($message); } @@ -86,7 +122,7 @@ public function send($view, array $data = [], $callback = null) * * Parameters: * - $view: View code as a string - * - $message: Illuminate\Mail\Message object, check Swift_Mime_SimpleMessage for useful functions. + * - $message: Illuminate\Mail\Message object, check Symfony\Component\Mime\Email for useful functions. * - $data: Array * * Example usage (stops the sending process): @@ -106,85 +142,169 @@ public function send($view, array $data = [], $callback = null) ($this->fireEvent('mailer.prepareSend', [$view, $message, $data], true) === false) || (Event::fire('mailer.prepareSend', [$this, $view, $message, $data], true) === false) ) { - return; + return null; } - /* - * Send the message + + + // Next we will determine if the message should be sent. We give the developer + // one final chance to stop this message and then we will send it to all of + // its recipients. We will then fire the sent event for the sent message. + $symfonyMessage = $message->getSymfonyMessage(); + + $sentMessage = null; + if ($this->shouldSendMessage($symfonyMessage, $data)) { + $symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage); + + if ($symfonySentMessage) { + $sentMessage = new SentMessage($symfonySentMessage); + + $this->dispatchSentEvent($sentMessage, $data); + + /** + * @event mailer.send + * Fires after the message has been sent + * + * Example usage (logs the message): + * + * Event::listen('mailer.send', function ((\Winter\Storm\Mail\Mailer) $mailerInstance, (string) $view, (\Illuminate\Mail\Message) $message, (array) $data) { + * \Log::info("Message was rendered with $view and sent"); + * }); + * + * Or + * + * $mailerInstance->bindEvent('mailer.send', function ((string) $view, (\Illuminate\Mail\Message) $message, (array) $data) { + * \Log::info("Message was rendered with $view and sent"); + * }); + * + */ + $this->fireEvent('mailer.send', [$view, $message, $data]); + Event::fire('mailer.send', [$this, $view, $message, $data]); + + return $sentMessage; + } + } + } + + /** + * Add the content to a given message. + * Overrides the Laravel defaults to provide the following functionality: + * - Events (global & local): + * - mailer.beforeAddContent + * - mailer.addContent + * - Support for the Winter MailParser + * + * @param \Illuminate\Mail\Message $message + * @param string|null $view + * @param string|null $plain + * @param string|null $raw + * @param array|null $data + * @return void + */ + protected function addContent($message, $view = null, $plain = null, $raw = null, $data = null) + { + /** + * @event mailer.beforeAddContent + * Fires before the mailer adds content to the message + * + * Example usage (stops the content adding process): + * + * Event::listen('mailer.beforeAddContent', function ((\Winter\Storm\Mail\Mailer) $mailerInstance, (\Illuminate\Mail\Message) $message, (string) $view, (array) $data, (string) $raw, (string) $plain) { + * return false; + * }); + * + * Or + * + * $mailerInstance->bindEvent('mailer.beforeAddContent', function ((\Illuminate\Mail\Message) $message, (string) $view, (array) $data, (string) $raw, (string) $plain) { + * return false; + * }); + * */ - $this->sendSwiftMessage($message->getSwiftMessage()); - $this->dispatchSentEvent($message); + if ( + ($this->fireEvent('mailer.beforeAddContent', [$message, $view, $data, $raw, $plain], true) === false) || + (Event::fire('mailer.beforeAddContent', [$this, $message, $view, $data, $raw, $plain], true) === false) + ) { + return; + } + + $html = null; + $text = null; + + if (isset($view)) { + $viewContent = $this->renderView($view, $data); + $result = MailParser::parse($viewContent); + $html = $result['html']; + + if ($result['text']) { + $text = $result['text']; + } + + /* + * Subject + */ + $customSubject = $message->getSymfonyMessage()->getSubject(); + if ( + empty($customSubject) && + ($subject = array_get($result['settings'], 'subject')) + ) { + $message->subject($subject); + } + } + + if (isset($plain)) { + $text = $this->renderView($plain, $data); + } + + if (isset($raw)) { + $text = $raw; + } + + $this->addContentRaw($message, $html, $text); /** - * @event mailer.send - * Fires after the message has been sent + * @event mailer.addContent + * Fires after the mailer has added content to the message * - * Example usage (logs the message): + * Example usage (Logs that content has been added): * - * Event::listen('mailer.send', function ((\Winter\Storm\Mail\Mailer) $mailerInstance, (string) $view, (\Illuminate\Mail\Message) $message, (array) $data) { - * \Log::info("Message was rendered with $view and sent"); + * Event::listen('mailer.addContent', function ((\Winter\Storm\Mail\Mailer) $mailerInstance, (\Illuminate\Mail\Message) $message, (string) $view, (array) $data) { + * \Log::info("$view has had content added to the message"); * }); * * Or * - * $mailerInstance->bindEvent('mailer.send', function ((string) $view, (\Illuminate\Mail\Message) $message, (array) $data) { - * \Log::info("Message was rendered with $view and sent"); + * $mailerInstance->bindEvent('mailer.addContent', function ((\Illuminate\Mail\Message) $message, (string) $view, (array) $data) { + * \Log::info("$view has had content added to the message"); * }); * */ - $this->fireEvent('mailer.send', [$view, $message, $data]); - Event::fire('mailer.send', [$this, $view, $message, $data]); + $this->fireEvent('mailer.addContent', [$message, $view, $data]); + Event::fire('mailer.addContent', [$this, $message, $view, $data]); } /** - * Helper for send() method, the first argument can take a single email or an - * array of recipients where the key is the address and the value is the name. + * Add the raw content to the provided message. * - * @param array $recipients - * @param string|array $view - * @param array $data - * @param mixed $callback - * @param array $options + * @param \Illuminate\Mail\Message $message + * @param string|null $html + * @param string|null $text * @return void */ - public function sendTo($recipients, $view, array $data = [], $callback = null, $options = []) + protected function addContentRaw($message, $html = null, $text = null) { - if ($callback && !$options && !is_callable($callback)) { - $options = $callback; + if (isset($html)) { + $message->html($html); } - if (is_bool($options)) { - $queue = $options; - $bcc = false; - } - else { - extract(array_merge([ - 'queue' => false, - 'bcc' => false - ], $options)); + if (isset($text)) { + $message->text($text); } - - $method = $queue === true ? 'queue' : 'send'; - $recipients = $this->processRecipients($recipients); - - return $this->{$method}($view, $data, function ($message) use ($recipients, $callback, $bcc) { - - $method = $bcc === true ? 'bcc' : 'to'; - - foreach ($recipients as $address => $name) { - $message->{$method}($address, $name); - } - - if (is_callable($callback)) { - $callback($message); - } - }); } /** * Queue a new e-mail message for sending. * - * @param string|array $view + * @param MailableContract|string|array $view * @param array $data * @param \Closure|string $callback * @param string|null $queue @@ -195,8 +315,7 @@ public function queue($view, $data = null, $callback = null, $queue = null) if (!$view instanceof MailableContract) { $mailable = $this->buildQueueMailable($view, $data, $callback, $queue); $queue = null; - } - else { + } else { $mailable = $view; $queue = $queue ?? $data; } @@ -222,7 +341,7 @@ public function queueOn($queue, $view, $data = null, $callback = null) * Queue a new e-mail message for sending after (n) seconds. * * @param int $delay - * @param string|array $view + * @param MailableContract|string|array $view * @param array $data * @param \Closure|string $callback * @param string|null $queue @@ -233,8 +352,7 @@ public function later($delay, $view, $data = null, $callback = null, $queue = nu if (!$view instanceof MailableContract) { $mailable = $this->buildQueueMailable($view, $data, $callback, $queue); $queue = null; - } - else { + } else { $mailable = $view; $queue = $queue ?? $data; } @@ -281,47 +399,69 @@ protected function buildQueueMailable($view, $data, $callback, $queueName = null } /** - * Send a new message when only a raw text part. - * - * @param string $text - * @param mixed $callback - * @return int + * Helper for raw() method, send a new message when only a raw text part. + * @param array $recipients + * @param array|string $view + * @param mixed $callback + * @param array $options + * @return \Illuminate\Mail\SentMessage|null */ - public function raw($view, $callback) + public function rawTo($recipients, $view, $callback = null, $options = []) { if (!is_array($view)) { $view = ['raw' => $view]; - } - elseif (!array_key_exists('raw', $view)) { + } elseif (!array_key_exists('raw', $view)) { $view['raw'] = true; } - return $this->send($view, [], $callback); + return $this->sendTo($recipients, $view, [], $callback, $options); } /** - * Helper for raw() method, send a new message when only a raw text part. + * Helper for send() method, the first argument can take a single email or an + * array of recipients where the key is the address and the value is the name. + * * @param array $recipients - * @param string $view - * @param mixed $callback - * @param array $options - * @return int + * @param string|array $view + * @param array $data + * @param mixed $callback + * @param array $options + * @return mixed */ - public function rawTo($recipients, $view, $callback = null, $options = []) + public function sendTo($recipients, $view, array $data = [], $callback = null, $options = []) { - if (!is_array($view)) { - $view = ['raw' => $view]; + if ($callback && !$options && !is_callable($callback)) { + $options = $callback; } - elseif (!array_key_exists('raw', $view)) { - $view['raw'] = true; + + if (is_bool($options)) { + $queue = $options; + $bcc = false; + } else { + $queue = (bool) ($options['queue'] ?? false); + $bcc = (bool) ($options['bcc'] ?? false); } - return $this->sendTo($recipients, $view, [], $callback, $options); + $method = $queue === true ? 'queue' : 'send'; + $recipients = $this->processRecipients($recipients); + + return $this->{$method}($view, $data, function ($message) use ($recipients, $callback, $bcc) { + $method = $bcc === true ? 'bcc' : 'to'; + + foreach ($recipients as $address => $name) { + $message->{$method}($address, $name); + } + + if (is_callable($callback)) { + $callback($message); + } + }); } /** * Process a recipients object, which can look like the following: - * - (string) admin@domain.tld + * - (string) 'admin@domain.tld' + * - (array) ['admin@domain.tld', 'other@domain.tld'] * - (object) ['email' => 'admin@domain.tld', 'name' => 'Adam Person'] * - (array) ['admin@domain.tld' => 'Adam Person', ...] * - (array) [ (object|array) ['email' => 'admin@domain.tld', 'name' => 'Adam Person'], [...] ] @@ -334,13 +474,14 @@ protected function processRecipients($recipients) if (is_string($recipients)) { $result[$recipients] = null; - } - elseif (is_array($recipients) || $recipients instanceof Collection) { + } elseif (is_array($recipients) || $recipients instanceof Collection) { foreach ($recipients as $address => $person) { - if (is_string($person)) { + if (is_int($address) && is_string($person)) { + // no name provided, only email address + $result[$person] = null; + } elseif (is_string($person)) { $result[$address] = $person; - } - elseif (is_object($person)) { + } elseif (is_object($person)) { if (empty($person->email) && empty($person->address)) { continue; } @@ -348,8 +489,7 @@ protected function processRecipients($recipients) $address = !empty($person->email) ? $person->email : $person->address; $name = !empty($person->name) ? $person->name : null; $result[$address] = $name; - } - elseif (is_array($person)) { + } elseif (is_array($person)) { if (!$address = array_get($person, 'email', array_get($person, 'address'))) { continue; } @@ -357,8 +497,7 @@ protected function processRecipients($recipients) $result[$address] = array_get($person, 'name'); } } - } - elseif (is_object($recipients)) { + } elseif (is_object($recipients)) { if (!empty($recipients->email) || !empty($recipients->address)) { $address = !empty($recipients->email) ? $recipients->email : $recipients->address; $name = !empty($recipients->name) ? $recipients->name : null; @@ -369,116 +508,6 @@ protected function processRecipients($recipients) return $result; } - /** - * Add the content to a given message. - * - * @param \Illuminate\Mail\Message $message - * @param string $view - * @param string $plain - * @param string $raw - * @param array $data - * @return void - */ - protected function addContent($message, $view, $plain, $raw, $data) - { - /** - * @event mailer.beforeAddContent - * Fires before the mailer adds content to the message - * - * Example usage (stops the content adding process): - * - * Event::listen('mailer.beforeAddContent', function ((\Winter\Storm\Mail\Mailer) $mailerInstance, (\Illuminate\Mail\Message) $message, (string) $view, (array) $data, (string) $raw, (string) $plain) { - * return false; - * }); - * - * Or - * - * $mailerInstance->bindEvent('mailer.beforeAddContent', function ((\Illuminate\Mail\Message) $message, (string) $view, (array) $data, (string) $raw, (string) $plain) { - * return false; - * }); - * - */ - if ( - ($this->fireEvent('mailer.beforeAddContent', [$message, $view, $data, $raw, $plain], true) === false) || - (Event::fire('mailer.beforeAddContent', [$this, $message, $view, $data, $raw, $plain], true) === false) - ) { - return; - } - - $html = null; - $text = null; - - if (isset($view)) { - $viewContent = $this->renderView($view, $data); - $result = MailParser::parse($viewContent); - $html = $result['html']; - - if ($result['text']) { - $text = $result['text']; - } - - /* - * Subject - */ - $customSubject = $message->getSwiftMessage()->getSubject(); - if ( - empty($customSubject) && - ($subject = array_get($result['settings'], 'subject')) - ) { - $message->subject($subject); - } - } - - if (isset($plain)) { - $text = $this->renderView($plain, $data); - } - - if (isset($raw)) { - $text = $raw; - } - - $this->addContentRaw($message, $html, $text); - - /** - * @event mailer.addContent - * Fires after the mailer has added content to the message - * - * Example usage (Logs that content has been added): - * - * Event::listen('mailer.addContent', function ((\Winter\Storm\Mail\Mailer) $mailerInstance, (\Illuminate\Mail\Message) $message, (string) $view, (array) $data) { - * \Log::info("$view has had content added to the message"); - * }); - * - * Or - * - * $mailerInstance->bindEvent('mailer.addContent', function ((\Illuminate\Mail\Message) $message, (string) $view, (array) $data) { - * \Log::info("$view has had content added to the message"); - * }); - * - */ - $this->fireEvent('mailer.addContent', [$message, $view, $data]); - Event::fire('mailer.addContent', [$this, $message, $view, $data]); - } - - /** - * Add the raw content to a given message. - * - * @param \Illuminate\Mail\Message $message - * @param string $html - * @param string $text - * @return void - */ - protected function addContentRaw($message, $html, $text) - { - if (isset($html)) { - $message->setBody($html, 'text/html'); - } - - if (isset($text)) { - $message->addPart($text, 'text/plain'); - } - } - /** * Tell the mailer to not really send messages. * @@ -488,12 +517,10 @@ protected function addContentRaw($message, $html, $text) public function pretend($value = true) { if ($value) { - $this->pretendingOriginal = Config::get('mail.driver'); - - Config::set('mail.driver', 'log'); - } - else { - Config::set('mail.driver', $this->pretendingOriginal); + $this->pretendingOriginal = Config::get('mail.default', 'smtp'); + Config::set('mail.default', 'log'); + } else { + Config::set('mail.default', $this->pretendingOriginal); } } } diff --git a/src/Mail/Transport/MandrillTransport.php b/src/Mail/Transport/MandrillTransport.php deleted file mode 100644 index 09a8991d9..000000000 --- a/src/Mail/Transport/MandrillTransport.php +++ /dev/null @@ -1,104 +0,0 @@ -key = $key; - $this->client = $client; - } - - /** - * {@inheritdoc} - */ - public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null) - { - $this->beforeSendPerformed($message); - - $this->client->request('POST', 'https://mandrillapp.com/api/1.0/messages/send-raw.json', [ - 'form_params' => [ - 'key' => $this->key, - 'to' => $this->getTo($message), - 'raw_message' => $message->toString(), - 'async' => true, - ], - ]); - - $this->sendPerformed($message); - - return $this->numberOfRecipients($message); - } - - /** - * Get all the addresses this message should be sent to. - * - * Note that Mandrill still respects CC, BCC headers in raw message itself. - * - * @param \Swift_Mime_SimpleMessage $message - * @return array - */ - protected function getTo(Swift_Mime_SimpleMessage $message) - { - $to = []; - - if ($message->getTo()) { - $to = array_merge($to, array_keys($message->getTo())); - } - - if ($message->getCc()) { - $to = array_merge($to, array_keys($message->getCc())); - } - - if ($message->getBcc()) { - $to = array_merge($to, array_keys($message->getBcc())); - } - - return $to; - } - - /** - * Get the API key being used by the transport. - * - * @return string - */ - public function getKey() - { - return $this->key; - } - - /** - * Set the API key being used by the transport. - * - * @param string $key - * @return string - */ - public function setKey($key) - { - return $this->key = $key; - } -} diff --git a/src/Mail/Transport/SparkPostTransport.php b/src/Mail/Transport/SparkPostTransport.php deleted file mode 100644 index 1cded0c40..000000000 --- a/src/Mail/Transport/SparkPostTransport.php +++ /dev/null @@ -1,170 +0,0 @@ -key = $key; - $this->client = $client; - $this->options = $options; - } - - /** - * {@inheritdoc} - */ - public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null) - { - $this->beforeSendPerformed($message); - - $recipients = $this->getRecipients($message); - - $message->setBcc([]); - - $response = $this->client->request('POST', $this->getEndpoint(), [ - 'headers' => [ - 'Authorization' => $this->key, - ], - 'json' => array_merge([ - 'recipients' => $recipients, - 'content' => [ - 'email_rfc822' => $message->toString(), - ], - ], $this->options), - ]); - - $message->getHeaders()->addTextHeader( - 'X-SparkPost-Transmission-ID', - $this->getTransmissionId($response) - ); - - $this->sendPerformed($message); - - return $this->numberOfRecipients($message); - } - - /** - * Get all the addresses this message should be sent to. - * - * Note that SparkPost still respects CC, BCC headers in raw message itself. - * - * @param \Swift_Mime_SimpleMessage $message - * @return array - */ - protected function getRecipients(Swift_Mime_SimpleMessage $message) - { - $recipients = []; - - foreach ((array) $message->getTo() as $email => $name) { - $recipients[] = ['address' => compact('name', 'email')]; - } - - foreach ((array) $message->getCc() as $email => $name) { - $recipients[] = ['address' => compact('name', 'email')]; - } - - foreach ((array) $message->getBcc() as $email => $name) { - $recipients[] = ['address' => compact('name', 'email')]; - } - - return $recipients; - } - - /** - * Get the transmission ID from the response. - * - * @param \GuzzleHttp\Psr7\Response $response - * @return string - */ - protected function getTransmissionId($response) - { - return object_get( - json_decode($response->getBody()->getContents()), - 'results.id' - ); - } - - /** - * Get the API key being used by the transport. - * - * @return string - */ - public function getKey() - { - return $this->key; - } - - /** - * Set the API key being used by the transport. - * - * @param string $key - * @return string - */ - public function setKey($key) - { - return $this->key = $key; - } - - /** - * Get the SparkPost API endpoint. - * - * @return string - */ - public function getEndpoint() - { - return $this->getOptions()['endpoint'] ?? 'https://api.sparkpost.com/api/v1/transmissions'; - } - - /** - * Get the transmission options being used by the transport. - * - * @return array - */ - public function getOptions() - { - return $this->options; - } - - /** - * Set the transmission options being used by the transport. - * - * @param array $options - * @return array - */ - public function setOptions(array $options) - { - return $this->options = $options; - } -} diff --git a/src/Mail/TransportManager.php b/src/Mail/TransportManager.php deleted file mode 100644 index 800542061..000000000 --- a/src/Mail/TransportManager.php +++ /dev/null @@ -1,39 +0,0 @@ -container['config']->get('services.mandrill', []); - - return new MandrillTransport( - $this->guzzle($config), - $config['secret'] - ); - } - - /** - * Create an instance of the SparkPost Swift Transport driver. - * - * @return \Winter\Storm\Mail\Transport\SparkPostTransport - */ - protected function createSparkPostDriver() - { - $config = $this->container['config']->get('services.sparkpost', []); - - return new SparkPostTransport( - $this->guzzle($config), - $config['secret'], - $config['options'] ?? [] - ); - } -} diff --git a/src/Network/Http.php b/src/Network/Http.php index e5cb3a373..a58bd9d27 100644 --- a/src/Network/Http.php +++ b/src/Network/Http.php @@ -94,7 +94,7 @@ class Http public $rawBody = ''; /** - * @var array The last returned HTTP code. + * @var int The last returned HTTP code. */ public $code; @@ -331,6 +331,18 @@ public function send() stream_filter_append($stream, $this->streamFilter, STREAM_FILTER_WRITE); } + if ($headerStream === false) { + throw new ApplicationException('Unable to create a temporary header stream'); + } + if ($stream === false) { + throw new ApplicationException( + sprintf( + 'Unable to stream file contents from HTTP response to "%s". Please check your permissions.', + $this->streamFile + ) + ); + } + curl_setopt($curl, CURLOPT_HEADER, false); curl_setopt($curl, CURLOPT_WRITEHEADER, $headerStream); curl_setopt($curl, CURLOPT_FILE, $stream); @@ -356,7 +368,7 @@ public function send() */ curl_close($curl); - if ($this->streamFile) { + if ($this->streamFile && !empty($stream) && !empty($headerStream)) { rewind($headerStream); $this->headers = $this->headerToArray(stream_get_contents($headerStream)); fclose($headerStream); @@ -556,9 +568,13 @@ public function toFile($path, $filter = null) } /** - * Add a single option to the request. - * @param string $option - * @param string $value + * Add single or multiple CURL options to this request. + * + * You must either provide a constant or string that represents a CURL_* constant as the $option, + * and a $value to set a single option, or you may provide an array of CURL_* constants and values instead. + * + * @param array|string|int $option + * @param mixed $value * @return self */ public function setOption($option, $value = null) diff --git a/src/Parse/Assetic/Cache/FilesystemCache.php b/src/Parse/Assetic/Cache/FilesystemCache.php new file mode 100644 index 000000000..e0d5bd3ee --- /dev/null +++ b/src/Parse/Assetic/Cache/FilesystemCache.php @@ -0,0 +1,29 @@ +dir) && false === @mkdir($this->dir, 0777, true)) { + throw new RuntimeException('Unable to create directory '.$this->dir); + } + + $path = $this->dir.'/'.$key; + + if (false === @file_put_contents($path, $value)) { + throw new RuntimeException('Unable to write file '.$path); + } + + File::chmod($path); + } +} diff --git a/src/Assetic/Filter/JavascriptImporter.php b/src/Parse/Assetic/Filter/JavascriptImporter.php similarity index 91% rename from src/Assetic/Filter/JavascriptImporter.php rename to src/Parse/Assetic/Filter/JavascriptImporter.php index c345132f7..a0ef8bd5a 100644 --- a/src/Assetic/Filter/JavascriptImporter.php +++ b/src/Parse/Assetic/Filter/JavascriptImporter.php @@ -1,9 +1,9 @@ -scriptPath = dirname($asset->getSourceRoot() . '/' . $asset->getSourcePath()); @@ -54,13 +52,13 @@ public function filterDump(AssetInterface $asset) } /** - * Process JS imports inside a string of javascript - * @param $content string JS code to process. + * Process JS imports inside a string of JavaScript + * + * @param string $content JS code to process. * @return string Processed JS. */ protected function parse($content) { - $macros = []; $imported = ''; // Look for: /* comments */ diff --git a/src/Assetic/Filter/LessCompiler.php b/src/Parse/Assetic/Filter/LessCompiler.php similarity index 77% rename from src/Assetic/Filter/LessCompiler.php rename to src/Parse/Assetic/Filter/LessCompiler.php index 828000bbb..f8fbd1cd2 100644 --- a/src/Assetic/Filter/LessCompiler.php +++ b/src/Parse/Assetic/Filter/LessCompiler.php @@ -1,21 +1,22 @@ -setContent($parser->getCss()); } - public function filterDump(AssetInterface $asset) - { - } - public function hashAsset($asset, $localPath) { $factory = new AssetFactory($localPath); diff --git a/src/Assetic/Filter/ScssCompiler.php b/src/Parse/Assetic/Filter/ScssCompiler.php similarity index 80% rename from src/Assetic/Filter/ScssCompiler.php rename to src/Parse/Assetic/Filter/ScssCompiler.php index 7bca60637..59b0d5ae5 100644 --- a/src/Assetic/Filter/ScssCompiler.php +++ b/src/Parse/Assetic/Filter/ScssCompiler.php @@ -1,16 +1,15 @@ - [] ]; - public function __construct($options = []) + final public function __construct($options = []) { $this->setOptions($options); } @@ -34,7 +34,7 @@ public function setOptions($options = []) * @param string $template * @param array $vars * @param array $options - * @return self + * @return string */ public static function parse($template, $vars = [], $options = []) { @@ -43,15 +43,16 @@ public static function parse($template, $vars = [], $options = []) } /** - * Parse a string against data + * Parse a string against data. + * * @param string $string * @param array $data * @return string */ public function parseString($string, $data) { - if (!is_string($string) || !strlen(trim($string))) { - return false; + if (!strlen(trim($string))) { + return ''; } foreach ($data as $key => $value) { diff --git a/src/Parse/Contracts/DataFileInterface.php b/src/Parse/Contracts/DataFileInterface.php new file mode 100644 index 000000000..949fc0066 --- /dev/null +++ b/src/Parse/Contracts/DataFileInterface.php @@ -0,0 +1,24 @@ +filePath = $filePath; + + list($this->env, $this->map) = $this->parse($filePath); + } + + /** + * Return a new instance of `EnvFile` ready for modification of the file. + */ + public static function open(?string $filePath = null): static + { + if (!$filePath) { + $filePath = base_path('.env'); + } + + return new static($filePath); + } + + /** + * Set a property within the env. Passing an array as param 1 is also supported. + * + * ```php + * $env->set('APP_PROPERTY', 'example'); + * // or + * $env->set([ + * 'APP_PROPERTY' => 'example', + * 'DIF_PROPERTY' => 'example' + * ]); + * ``` + */ + public function set(array|string $key, $value = null): static + { + if (is_array($key)) { + foreach ($key as $item => $value) { + $this->set($item, $value); + } + return $this; + } + + if (!isset($this->map[$key])) { + $this->env[] = [ + 'type' => 'var', + 'key' => $key, + 'value' => $value + ]; + + $this->map[$key] = count($this->env) - 1; + + return $this; + } + + $this->env[$this->map[$key]]['value'] = $value; + + return $this; + } + + /** + * Push a newline onto the end of the env file + */ + public function addEmptyLine(): EnvFile + { + $this->env[] = [ + 'type' => 'nl' + ]; + + return $this; + } + + /** + * Write the current env lines to a fileh + */ + public function write(string $filePath = null): void + { + if (!$filePath) { + $filePath = $this->filePath; + } + + file_put_contents($filePath, $this->render()); + } + + /** + * Get the env lines data as a string + */ + public function render(): string + { + $out = ''; + foreach ($this->env as $env) { + switch ($env['type']) { + case 'comment': + $out .= $env['value']; + break; + case 'var': + $out .= $env['key'] . '=' . $this->escapeValue($env['value']); + break; + } + + $out .= PHP_EOL; + } + + return $out; + } + + /** + * Wrap a value in quotes if needed + * + * @param mixed $value + */ + protected function escapeValue($value): string + { + if (is_numeric($value)) { + return $value; + } + + if ($value === true) { + return 'true'; + } + + if ($value === false) { + return 'false'; + } + + if ($value === null) { + return 'null'; + } + + switch ($value) { + case 'true': + case 'false': + case 'null': + return $value; + default: + // addslashes() wont work as it'll escape single quotes and they will be read literally + return '"' . Str::replace('"', '\"', $value) . '"'; + } + } + + /** + * Parse a .env file, returns an array of the env file data and a key => position map + */ + protected function parse(string $filePath): array + { + if (!is_file($filePath)) { + return [[], []]; + } + + $contents = file($filePath); + if (empty($contents)) { + return [[], []]; + } + + $env = []; + $map = []; + + foreach ($contents as $line) { + $type = !($line = trim($line)) + ? 'nl' + : ( + Str::startsWith($line, '#') + ? 'comment' + : 'var' + ); + + $entry = [ + 'type' => $type + ]; + + if ($type === 'var') { + if (strpos($line, '=') === false) { + // if we cannot split the string, handle it the same as a comment + // i.e. inject it back into the file as is + $entry['type'] = $type = 'comment'; + } else { + list($key, $value) = explode('=', $line); + $entry['key'] = trim($key); + $entry['value'] = trim($value, '"'); + } + } + + if ($type === 'comment') { + $entry['value'] = $line; + } + + $env[] = $entry; + } + + foreach ($env as $index => $item) { + if ($item['type'] !== 'var') { + continue; + } + $map[$item['key']] = $index; + } + + return [$env, $map]; + } + + /** + * Get the variables from the current env lines data as an associative array + */ + public function getVariables(): array + { + $env = []; + + foreach ($this->env as $item) { + if ($item['type'] !== 'var') { + continue; + } + $env[$item['key']] = $item['value']; + } + + return $env; + } +} diff --git a/src/Parse/Ini.php b/src/Parse/Ini.php index 60e15bba9..82e112de7 100644 --- a/src/Parse/Ini.php +++ b/src/Parse/Ini.php @@ -110,11 +110,11 @@ protected function parsePostProcess($array) * Expands a single array property from traditional INI syntax. * If no key is given to the method, the entire array will be replaced. * @param array $array - * @param string $key + * @param string|null $key * @param mixed $value * @return array */ - public function expandProperty(&$array, $key, $value) + public function expandProperty(array &$array, $key = null, $value = null) { if (is_null($key)) { return $array = $value; diff --git a/src/Parse/Markdown.php b/src/Parse/Markdown.php index 7e31f5e14..5146257d1 100644 --- a/src/Parse/Markdown.php +++ b/src/Parse/Markdown.php @@ -1,6 +1,6 @@ parserClass; + } + + /** + * Sets the Markdown parser. + * + * @param string|object $parserClass + * @return void + */ + public function setParser(string|object $parserClass) + { + if (is_object($parserClass)) { + $this->parserClass = get_class($parserClass); + } else { + $this->parserClass = $parserClass; + } + } /** * Parse text using Markdown and Markdown-Extra - * @param string $text Markdown text to parse - * @return string Resulting HTML + * @param string $text Markdown text to parse + * @return string Resulting HTML */ public function parse($text) { @@ -52,13 +79,9 @@ public function parse($text) */ public function parseClean($text) { - $this->getParser()->setSafeMode(true); - - $result = $this->parse($text); - - $this->parser = null; + $parser = $this->getParser()->setSafeMode(true); - return $result; + return $this->parseInternal($text, 'text', $parser); } /** @@ -68,13 +91,9 @@ public function parseClean($text) */ public function parseSafe($text) { - $this->getParser()->setUnmarkedBlockTypes([]); + $parser = $this->getParser()->setUnmarkedBlockTypes([]); - $result = $this->parse($text); - - $this->parser = null; - - return $result; + return $this->parseInternal($text, 'text', $parser); } /** @@ -90,16 +109,19 @@ public function parseLine($text) /** * Internal method for parsing */ - protected function parseInternal($text, $method = 'text') + protected function parseInternal($text, $method = 'text', Parsedown $parser = null) { + if (is_null($parser)) { + $parser = $this->getParser(); + } $data = new MarkdownData($text); - $this->fireEvent('beforeParse', $data, false); - Event::fire('markdown.beforeParse', $data, false); + $this->fireEvent('beforeParse', [$data], false); + Event::fire('markdown.beforeParse', [$data], false); $result = $data->text; - $result = $this->getParser()->$method($result); + $result = $parser->$method($result); $data->text = $result; @@ -110,13 +132,4 @@ protected function parseInternal($text, $method = 'text') return $data->text; } - - protected function getParser() - { - if ($this->parser === null) { - $this->parser = new Parsedown; - } - - return $this->parser; - } } diff --git a/src/Parse/PHP/ArrayFile.php b/src/Parse/PHP/ArrayFile.php new file mode 100644 index 000000000..c0b2e7a18 --- /dev/null +++ b/src/Parse/PHP/ArrayFile.php @@ -0,0 +1,435 @@ +astReturnIndex = $this->getAstReturnIndex($ast); + + if (is_null($this->astReturnIndex)) { + throw new \InvalidArgumentException('ArrayFiles must start with a return statement'); + } + + $this->ast = $ast; + $this->lexer = $lexer; + $this->filePath = $filePath; + $this->printer = $printer ?? new ArrayPrinter(); + } + + /** + * Return a new instance of `ArrayFile` ready for modification of the file. + * + * @throws \InvalidArgumentException if the provided path doesn't exist and $throwIfMissing is true + * @throws SystemException if the provided path is unable to be parsed + */ + public static function open(string $filePath, bool $throwIfMissing = false): static + { + $exists = file_exists($filePath); + + if (!$exists && $throwIfMissing) { + throw new \InvalidArgumentException('file not found'); + } + + $lexer = new Lexer\Emulative([ + 'usedAttributes' => [ + 'comments', + 'startTokenPos', + 'startLine', + 'endTokenPos', + 'endLine' + ] + ]); + $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7, $lexer); + + try { + $ast = $parser->parse( + $exists + ? file_get_contents($filePath) + : sprintf('set('property.key.value', 'example'); + * // or + * $config->set([ + * 'property.key1.value' => 'example', + * 'property.key2.value' => 'example' + * ]); + * ``` + */ + public function set(string|array $key, $value = null): static + { + if (is_array($key)) { + foreach ($key as $name => $value) { + $this->set($name, $value); + } + + return $this; + } + + // try to find a reference to ast object + list($target, $remaining) = $this->seek(explode('.', $key), $this->ast[$this->astReturnIndex]->expr); + + $valueType = $this->getType($value); + + // part of a path found + if ($target && $remaining) { + $target->value->items[] = $this->makeArrayItem(implode('.', $remaining), $valueType, $value); + return $this; + } + + // path to not found + if (is_null($target)) { + $this->ast[$this->astReturnIndex]->expr->items[] = $this->makeArrayItem($key, $valueType, $value); + return $this; + } + + if (!isset($target->value)) { + return $this; + } + + // special handling of function objects + if (get_class($target->value) === FuncCall::class && $valueType !== 'function') { + if ($target->value->name->parts[0] !== 'env' || !isset($target->value->args[0])) { + return $this; + } + if (isset($target->value->args[0]) && !isset($target->value->args[1])) { + $target->value->args[1] = new Arg($this->makeAstNode($valueType, $value)); + } + $target->value->args[1]->value = $this->makeAstNode($valueType, $value); + return $this; + } + + // default update in place + $target->value = $this->makeAstNode($valueType, $value); + + return $this; + } + + /** + * Creates either a simple array item or a recursive array of items + */ + protected function makeArrayItem(string $key, string $valueType, $value): ArrayItem + { + return (str_contains($key, '.')) + ? $this->makeAstArrayRecursive($key, $valueType, $value) + : new ArrayItem( + $this->makeAstNode($valueType, $value), + $this->makeAstNode($this->getType($key), $key) + ); + } + + /** + * Generate an AST node, using `PhpParser` classes, for a value + * + * @throws \RuntimeException If $type is not one of 'string', 'boolean', 'integer', 'function', 'const', 'null', or 'array' + * @return ConstFetch|LNumber|String_|Array_|FuncCall + */ + protected function makeAstNode(string $type, $value) + { + switch (strtolower($type)) { + case 'string': + return new String_($value); + case 'boolean': + return new ConstFetch(new Name($value ? 'true' : 'false')); + case 'integer': + return new LNumber($value); + case 'function': + return new FuncCall( + new Name($value->getName()), + array_map(function ($arg) { + return new Arg($this->makeAstNode($this->getType($arg), $arg)); + }, $value->getArgs()) + ); + case 'const': + return new ConstFetch(new Name($value->getName())); + case 'null': + return new ConstFetch(new Name('null')); + case 'array': + return $this->castArray($value); + default: + throw new \RuntimeException("An unimlemented replacement type ($type) was encountered"); + } + } + + /** + * Cast an array to AST + */ + protected function castArray(array $array): Array_ + { + return ($caster = function ($array, $ast) use (&$caster) { + $useKeys = []; + foreach (array_keys($array) as $i => $key) { + $useKeys[$key] = (!is_numeric($key) || $key !== $i); + } + foreach ($array as $key => $item) { + if (is_array($item)) { + $ast->items[] = new ArrayItem( + $caster($item, new Array_()), + ($useKeys[$key] ? $this->makeAstNode($this->getType($key), $key) : null) + ); + continue; + } + $ast->items[] = new ArrayItem( + $this->makeAstNode($this->getType($item), $item), + ($useKeys[$key] ? $this->makeAstNode($this->getType($key), $key) : null) + ); + } + + return $ast; + })($array, new Array_()); + } + + /** + * Returns type of var passed + * + * @param mixed $var + */ + protected function getType($var): string + { + if ($var instanceof PHPFunction) { + return 'function'; + } + + if ($var instanceof PHPConstant) { + return 'const'; + } + + return gettype($var); + } + + /** + * Returns an ArrayItem generated from a dot notation path + * + * @param string $key + * @param string $valueType + * @param mixed $value + */ + protected function makeAstArrayRecursive(string $key, string $valueType, $value): ArrayItem + { + $path = array_reverse(explode('.', $key)); + + $arrayItem = $this->makeAstNode($valueType, $value); + + foreach ($path as $index => $pathKey) { + if (is_numeric($pathKey)) { + $pathKey = (int) $pathKey; + } + $arrayItem = new ArrayItem($arrayItem, $this->makeAstNode($this->getType($pathKey), $pathKey)); + + if ($index !== array_key_last($path)) { + $arrayItem = new Array_([$arrayItem]); + } + } + + return $arrayItem; + } + + /** + * Find the return position within the ast, returns null on encountering an unsupported ast stmt. + * + * @param array $ast + * @return int|null + */ + protected function getAstReturnIndex(array $ast): ?int + { + foreach ($ast as $index => $item) { + switch (get_class($item)) { + case Stmt\Use_::class: + case Stmt\Expression::class: + break; + case Stmt\Return_::class: + return $index; + default: + return null; + } + } + + return null; + } + + /** + * Attempt to find the parent object of the targeted path. + * If the path cannot be found completely, return the nearest parent and the remainder of the path + * + * @param array $path + * @param mixed $pointer + * @param int $depth + * @throws SystemException if trying to set a position that is already occupied by a value + */ + protected function seek(array $path, &$pointer, int $depth = 0): array + { + if (!$pointer) { + return [null, $path]; + } + + $key = array_shift($path); + + if (isset($pointer->value) && !($pointer->value instanceof ArrayItem || $pointer->value instanceof Array_)) { + throw new SystemException(sprintf( + 'Illegal offset, you are trying to set a position occupied by a value (%s)', + get_class($pointer->value) + )); + } + + foreach (($pointer->items ?? $pointer->value->items) as $index => &$item) { + // loose checking to allow for int keys + if ($item->key->value == $key) { + if (!empty($path)) { + return $this->seek($path, $item, ++$depth); + } + + return [$item, []]; + } + } + + array_unshift($path, $key); + + return [($depth > 0) ? $pointer : null, $path]; + } + + /** + * Sort the config, supports: ArrayFile::SORT_ASC, ArrayFile::SORT_DESC, callable + * + * @param string|callable $mode + * @throws \InvalidArgumentException if the provided sort type is not a callable or one of static::SORT_ASC or static::SORT_DESC + */ + public function sort($mode = self::SORT_ASC): ArrayFile + { + if (is_callable($mode)) { + usort($this->ast[0]->expr->items, $mode); + return $this; + } + + switch ($mode) { + case static::SORT_ASC: + case static::SORT_DESC: + $this->sortRecursive($this->ast[0]->expr->items, $mode); + break; + default: + throw new \InvalidArgumentException('Requested sort type is invalid'); + } + + return $this; + } + + /** + * Recursive sort an Array_ item array + */ + protected function sortRecursive(array &$array, string $mode): void + { + foreach ($array as &$item) { + if (isset($item->value) && $item->value instanceof Array_) { + $this->sortRecursive($item->value->items, $mode); + } + } + + usort($array, function ($a, $b) use ($mode) { + return $mode === static::SORT_ASC + ? $a->key->value <=> $b->key->value + : $b->key->value <=> $a->key->value; + }); + } + + /** + * Write the current config to a file + */ + public function write(string $filePath = null): void + { + if (!$filePath && $this->filePath) { + $filePath = $this->filePath; + } + + file_put_contents($filePath, $this->render()); + } + + /** + * Returns a new instance of PHPFunction + */ + public function function(string $name, array $args): PHPFunction + { + return new PHPFunction($name, $args); + } + + /** + * Returns a new instance of PHPConstant + */ + public function constant(string $name): PHPConstant + { + return new PHPConstant($name); + } + + /** + * Get the printed AST as PHP code + */ + public function render(): string + { + return $this->printer->render($this->ast, $this->lexer) . "\n"; + } + + /** + * Get currently loaded AST + * + * @return Stmt[]|null + */ + public function getAst() + { + return $this->ast; + } +} diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php new file mode 100644 index 000000000..81f967daf --- /dev/null +++ b/src/Parse/PHP/ArrayPrinter.php @@ -0,0 +1,316 @@ +lexer = $lexer; + + $p = "prettyPrint($stmts); + + if ($stmts[0] instanceof Stmt\InlineHTML) { + $p = preg_replace('/^<\?php\s+\?>\n?/', '', $p); + } + if ($stmts[count($stmts) - 1] instanceof Stmt\InlineHTML) { + $p = preg_replace('/<\?php$/', '', rtrim($p)); + } + + $this->lexer = null; + + return $p; + } + + /** + * @param array $nodes + * @param bool $trailingComma + * @return string + */ + protected function pMaybeMultiline(array $nodes, bool $trailingComma = false) + { + if ($this->hasNodeWithComments($nodes) || (isset($nodes[0]) && $nodes[0] instanceof Expr\ArrayItem)) { + return $this->pCommaSeparatedMultiline($nodes, $trailingComma) . $this->nl; + } else { + return $this->pCommaSeparated($nodes); + } + } + + /** + * Pretty prints a comma-separated list of nodes in multiline style, including comments. + * + * The result includes a leading newline and one level of indentation (same as pStmts). + * + * @param array $nodes Array of Nodes to be printed + * @param bool $trailingComma Whether to use a trailing comma + * + * @return string Comma separated pretty printed nodes in multiline style + */ + protected function pCommaSeparatedMultiline(array $nodes, bool $trailingComma): string + { + $this->indent(); + + $result = ''; + $lastIdx = count($nodes) - 1; + foreach ($nodes as $idx => $node) { + if ($node !== null) { + $comments = $node->getComments(); + + if ($comments) { + $result .= $this->pComments($comments); + } + + $result .= $this->nl . $this->p($node); + } else { + $result = trim($result) . "\n"; + } + if ($trailingComma || $idx !== $lastIdx) { + $result .= ','; + } + } + + $this->outdent(); + return $result; + } + + /** + * Render an array expression + * + * @param Expr\Array_ $node Array expression node + * + * @return string Comma separated pretty printed nodes in multiline style + */ + protected function pExpr_Array(Expr\Array_ $node): string + { + $default = $this->options['shortArraySyntax'] + ? Expr\Array_::KIND_SHORT + : Expr\Array_::KIND_LONG; + + $ops = $node->getAttribute('kind', $default) === Expr\Array_::KIND_SHORT + ? ['[', ']'] + : ['array(', ')']; + + if (!count($node->items) && $comments = $this->getNodeComments($node)) { + // the array has no items, we can inject whatever we want + return sprintf( + '%s%s%s%s%s', + // opening control char + $ops[0], + // indent and add nl string + $this->indent(), + // join all comments with nl string + implode($this->nl, $comments), + // outdent and add nl string + $this->outdent(), + // closing control char + $ops[1] + ); + } + + if ($comments = $this->getCommentsNotInArray($node)) { + // array has items, we have detected comments not included within the array, therefore we have found + // trailing comments and must append them to the end of the array + return sprintf( + '%s%s%s%s%s%s', + // opening control char + $ops[0], + // render the children + $this->pMaybeMultiline($node->items, true), + // add 1 level of indentation + str_repeat(' ', 4), + // join all comments with the current indentation + implode($this->nl . str_repeat(' ', 4), $comments), + // add a trailing nl + $this->nl, + // closing control char + $ops[1] + ); + } + + // default return + return $ops[0] . $this->pMaybeMultiline($node->items, true) . $ops[1]; + } + + /** + * Increase indentation level. + * Proxied to allow for nl return + * + * @return string + */ + protected function indent(): string + { + $this->indentLevel += 4; + $this->nl .= ' '; + return $this->nl; + } + + /** + * Decrease indentation level. + * Proxied to allow for nl return + * + * @return string + */ + protected function outdent(): string + { + assert($this->indentLevel >= 4); + $this->indentLevel -= 4; + $this->nl = "\n" . str_repeat(' ', $this->indentLevel); + return $this->nl; + } + + /** + * Get all comments that have not been attributed to a node within a node array + * + * @param Expr\Array_ $nodes Array of nodes + * + * @return array Comments found + */ + protected function getCommentsNotInArray(Expr\Array_ $nodes): array + { + if (!$comments = $this->getNodeComments($nodes)) { + return []; + } + + return array_filter($comments, function ($comment) use ($nodes) { + return !$this->commentInNodeList($nodes->items, $comment); + }); + } + + /** + * Recursively check if a comment exists in an array of nodes + * + * @param Node[] $nodes Array of nodes + * @param string $comment The comment to search for + * + * @return bool + */ + protected function commentInNodeList(array $nodes, string $comment): bool + { + foreach ($nodes as $node) { + if ($node->value instanceof Expr\Array_ && $this->commentInNodeList($node->value->items, $comment)) { + return true; + } + if ($nodeComments = $node->getAttribute('comments')) { + foreach ($nodeComments as $nodeComment) { + if ($nodeComment->getText() === $comment) { + return true; + } + } + } + } + + return false; + } + + /** + * Check the lexer tokens for comments within the node's start & end position + * + * @param Node $node Node to check + * + * @return ?array + */ + protected function getNodeComments(Node $node): ?array + { + $tokens = $this->lexer->getTokens(); + $pos = $node->getAttribute('startTokenPos'); + $end = $node->getAttribute('endTokenPos'); + $endLine = $node->getAttribute('endLine'); + $content = []; + + while (++$pos < $end) { + if (!isset($tokens[$pos]) || (!is_array($tokens[$pos]) && $tokens[$pos] !== ',')) { + break; + } + + if ($tokens[$pos][0] === T_WHITESPACE || $tokens[$pos] === ',') { + continue; + } + + list($type, $string, $line) = $tokens[$pos]; + + if ($line > $endLine) { + break; + } + + if ($type === T_COMMENT || $type === T_DOC_COMMENT) { + $content[] = $string; + } elseif ($content) { + break; + } + } + + return empty($content) ? null : $content; + } + + /** + * Prints reformatted text of the passed comments. + * + * @param array $comments List of comments + * + * @return string Reformatted text of comments + */ + protected function pComments(array $comments): string + { + $formattedComments = []; + + foreach ($comments as $comment) { + $formattedComments[] = str_replace("\n", $this->nl, $comment->getReformattedText()); + } + + $padding = $comments[0]->getStartLine() !== $comments[count($comments) - 1]->getEndLine() ? $this->nl : ''; + + return "\n" . $this->nl . trim($padding . implode($this->nl, $formattedComments)) . "\n"; + } + + protected function pExpr_Include(Expr\Include_ $node) + { + static $map = [ + Expr\Include_::TYPE_INCLUDE => 'include', + Expr\Include_::TYPE_INCLUDE_ONCE => 'include_once', + Expr\Include_::TYPE_REQUIRE => 'require', + Expr\Include_::TYPE_REQUIRE_ONCE => 'require_once', + ]; + + return $map[$node->type] . '(' . $this->p($node->expr) . ')'; + } +} diff --git a/src/Parse/PHP/PHPConstant.php b/src/Parse/PHP/PHPConstant.php new file mode 100644 index 000000000..887a9eac5 --- /dev/null +++ b/src/Parse/PHP/PHPConstant.php @@ -0,0 +1,25 @@ +name = $name; + } + + /** + * Get the const name + */ + public function getName(): string + { + return $this->name; + } +} diff --git a/src/Parse/PHP/PHPFunction.php b/src/Parse/PHP/PHPFunction.php new file mode 100644 index 000000000..1874b1e26 --- /dev/null +++ b/src/Parse/PHP/PHPFunction.php @@ -0,0 +1,47 @@ +name = $name; + $this->args = $args; + } + + /** + * Get the function name + * + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * Get the function arguments + * + * @return array + */ + public function getArgs(): array + { + return $this->args; + } +} diff --git a/src/Parse/ParseServiceProvider.php b/src/Parse/ParseServiceProvider.php index 999887ff5..03fb6d906 100644 --- a/src/Parse/ParseServiceProvider.php +++ b/src/Parse/ParseServiceProvider.php @@ -1,6 +1,7 @@ Markdown::class, - 'parse.yaml' => Yaml::class, 'parse.twig' => Twig::class, 'parse.ini' => Ini::class, ]; + /** + * Register the service provider. + * @return void + */ + public function register() + { + $this->app->singleton('parse.yaml', function ($app) { + $yaml = new Yaml(); + $yaml->setProcessor(new Symfony3Processor); + + return $yaml; + }); + } + /** * Get the services provided by the provider. * diff --git a/src/Parse/Processor/Contracts/YamlProcessor.php b/src/Parse/Processor/Contracts/YamlProcessor.php index ed40b4edb..c9b5f5048 100644 --- a/src/Parse/Processor/Contracts/YamlProcessor.php +++ b/src/Parse/Processor/Contracts/YamlProcessor.php @@ -3,7 +3,7 @@ /** * Yaml processor contract. * - * Allows for pre-or-post processing of YAML content during parsing. + * Allows for pre-or-post processing of YAML content during parsing or rendering. * * @author Winter CMS */ @@ -24,4 +24,20 @@ public function preprocess($text); * @return mixed */ public function process($parsed); + + /** + * Pre-process the data that will be rendered to a YAML string or file. + * + * @param mixed $data + * @return mixed + */ + public function prerender($data); + + /** + * Post-process a rendered YAML string or file. + * + * @param string $yaml + * @return string + */ + public function render($yaml); } diff --git a/src/Parse/Processor/Symfony3Processor.php b/src/Parse/Processor/Symfony3Processor.php new file mode 100644 index 000000000..3a58cfe15 --- /dev/null +++ b/src/Parse/Processor/Symfony3Processor.php @@ -0,0 +1,39 @@ +tagPrefix = array_get($options, 'tagPrefix', ''); - $this->template = $template; - $this->processTemplate($template); - } + $this->tagPrefix = array_get($options, 'tagPrefix', ''); + $this->template = $template; + $this->processTemplate($template); } /** * Processes repeating tags first, then registered tags and assigns * the results to local object properties. + * + * @param string $template * @return void */ protected function processTemplate($template) @@ -103,7 +105,7 @@ protected function processTemplate($template) * Static helper for new instances of this class. * @param string $template * @param array $options - * @return FieldParser + * @return static */ public static function parse($template, $options = []) { @@ -181,7 +183,7 @@ public function getDefaultParams($fields = null) * Processes all repeating tags against a template, this will strip * any repeaters from the template for further processing. * @param string $template - * @return void + * @return array */ protected function processRepeaterTags($template) { @@ -213,8 +215,8 @@ protected function processRepeaterTags($template) /** * Processes all registered tags against a template. * @param string $template - * @param bool $usingTags - * @return void + * @param array $usingTags + * @return array */ protected function processTags($template, $usingTags = null) { @@ -375,7 +377,7 @@ protected function processParamsRegex($string) * 2 - The default text inside the tag (optional), eg: Foobar * * @param string $string - * @param string $tags + * @param array $tags * @return array */ protected function processTagsRegex($string, $tags) diff --git a/src/Parse/Syntax/Parser.php b/src/Parse/Syntax/Parser.php index ab842f17b..61de67454 100644 --- a/src/Parse/Syntax/Parser.php +++ b/src/Parse/Syntax/Parser.php @@ -10,6 +10,11 @@ class Parser const CHAR_OPEN = '{'; const CHAR_CLOSE = '}'; + /** + * @var string The template content to parse. + */ + protected $template = ''; + /** * @var \Winter\Storm\Parse\Syntax\FieldParser Field parser instance. */ @@ -28,33 +33,33 @@ class Parser /** * Constructor. + * * Available options: * - varPrefix: Prefix to add to every top level parameter. * - tagPrefix: Prefix to add to all tags, in addition to tags without a prefix. - * @param array $options + * * @param string $template Template to parse. + * @param array $options */ - public function __construct($template = null, $options = []) + final public function __construct($template, $options = []) { - if ($template) { - $this->template = $template; - $this->varPrefix = array_get($options, 'varPrefix', ''); - $this->fieldParser = new FieldParser($template, $options); + $this->template = $template; + $this->varPrefix = array_get($options, 'varPrefix', ''); + $this->fieldParser = new FieldParser($template, $options); - $textFilters = [ - 'md' => ['Markdown', 'parse'], - 'media' => ['System\Classes\MediaLibrary', 'url'] - ]; + $textFilters = [ + 'md' => ['Winter\Storm\Parse\Markdown', 'parse'], + 'media' => ['System\Classes\MediaLibrary', 'url'] + ]; - $this->textParser = new TextParser(['filters' => $textFilters]); - } + $this->textParser = new TextParser(['filters' => $textFilters]); } /** * Static helper for new instances of this class. * @param string $template * @param array $options - * @return self + * @return static */ public static function parse($template, $options = []) { diff --git a/src/Parse/Syntax/SyntaxModelTrait.php b/src/Parse/Syntax/SyntaxModelTrait.php index c49a25038..75b02f56b 100644 --- a/src/Parse/Syntax/SyntaxModelTrait.php +++ b/src/Parse/Syntax/SyntaxModelTrait.php @@ -1,6 +1,6 @@ processor)) { + // Only run the preprocessor if parsing fails + try { + $parsed = $yaml->parse($contents); + } catch (\Throwable $throwable) { + if (!$this->processor) { + throw $throwable; + } $contents = $this->processor->preprocess($contents); + $parsed = $yaml->parse($contents); } - $parsed = $yaml->parse($contents); - if (!is_null($this->processor)) { $parsed = $this->processor->process($parsed); } @@ -66,26 +72,40 @@ public function parseFile($fileName) /** * Renders a PHP array to YAML format. * - * @param array $vars - * @param array $options - * * Supported options: * - inline: The level where you switch to inline YAML. * - exceptionOnInvalidType: if an exception must be thrown on invalid types. * - objectSupport: if object support is enabled. - * - * @return string */ - public function render($vars = [], $options = []) + public function render(array $vars = [], array $options = []): string { - extract(array_merge([ - 'inline' => 20, - 'exceptionOnInvalidType' => false, - 'objectSupport' => true, - ], $options)); + $inline = (int) ($options['inline'] ?? 20); + $exceptionOnInvalidType = (bool) ($options['exceptionOnInvalidType'] ?? false); + $objectSupport = (bool) ($options['objectSupport'] ?? true); + + $flags = null; + + if ($exceptionOnInvalidType === true) { + $flags |= YamlComponent::DUMP_EXCEPTION_ON_INVALID_TYPE; + } + + if ($objectSupport === true) { + $flags |= YamlComponent::DUMP_OBJECT; + } $yaml = new Dumper; - return $yaml->dump($vars, $inline, 0, $exceptionOnInvalidType, $objectSupport); + + if (!is_null($this->processor) && method_exists($this->processor, 'prerender')) { + $vars = $this->processor->prerender($vars); + } + + $yamlContent = $yaml->dump($vars, $inline, 0, $flags); + + if (!is_null($this->processor) && method_exists($this->processor, 'render')) { + $yamlContent = $this->processor->render($yamlContent); + } + + return $yamlContent; } /** diff --git a/src/Router/CoreRouter.php b/src/Router/CoreRouter.php index 59a402cf7..bbc905a7b 100644 --- a/src/Router/CoreRouter.php +++ b/src/Router/CoreRouter.php @@ -9,7 +9,7 @@ class CoreRouter extends RouterBase * Dispatch the request to the application. * * @param \Illuminate\Http\Request $request - * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse + * @return \Symfony\Component\HttpFoundation\Response */ public function dispatch(Request $request) { diff --git a/src/Router/Helper.php b/src/Router/Helper.php index 2b636bce7..a610657cd 100644 --- a/src/Router/Helper.php +++ b/src/Router/Helper.php @@ -72,7 +72,7 @@ public static function rebuildUrl(array $urlArray) /** * Replaces :column_name with it's object value. Example: /some/link/:id/:name -> /some/link/1/Joe * - * @param stdObject $object Object containing the data + * @param object|array $object Object containing the data * @param array $columns Expected key names to parse * @param string $string URL template * @return string Built string @@ -102,7 +102,7 @@ public static function parseValues($object, array $columns, $string) /** * Replaces :column_name with object value without requiring a list of names. Example: /some/link/:id/:name -> /some/link/1/Joe * - * @param stdObject $object Object containing the data + * @param object|array $object Object containing the data * @param string $string URL template * @return string Built string */ @@ -144,11 +144,7 @@ public static function segmentIsOptional($segment) return true; } - if ($optMarkerPos !== false && $regexMarkerPos !== false) { - return $optMarkerPos < $regexMarkerPos; - } - - return false; + return $optMarkerPos < $regexMarkerPos; } /** @@ -194,7 +190,7 @@ public static function getParameterName($segment) /** * Extracts the regular expression from a URL pattern segment definition. * @param string $segment The segment definition. - * @return string Returns the regular expression string or false if the expression is not defined. + * @return string|false Returns the regular expression string or false if the expression is not defined. */ public static function getSegmentRegExp($segment) { @@ -213,7 +209,7 @@ public static function getSegmentRegExp($segment) /** * Extracts the default parameter value from a URL pattern segment definition. * @param string $segment The segment definition. - * @return string Returns the default value if it is provided. Returns false otherwise. + * @return string|false Returns the default value if it is provided. Returns false otherwise. */ public static function getSegmentDefaultValue($segment) { diff --git a/src/Router/Router.php b/src/Router/Router.php index b3fd5fd06..de4b9e313 100644 --- a/src/Router/Router.php +++ b/src/Router/Router.php @@ -20,7 +20,7 @@ class Router protected $routeMap = []; /** - * @var \Winter\Storm\Router\Rule A referred to the matched router rule + * @var \Winter\Storm\Router\Rule|null A referred to the matched router rule */ protected $matchedRouteRule; @@ -41,7 +41,7 @@ public function route($name, $route) * Match given URL string * * @param string $url Request URL to match for - * @return array $parameters A reference to a PHP array variable to return the parameter list fetched from URL. + * @return bool */ public function match($url) { @@ -73,9 +73,9 @@ public function match($url) } // Success - if ($this->matchedRouteRule) { + if (!is_null($this->matchedRouteRule)) { // If this route has a match callback, run it - $matchCallback = $routeRule->afterMatch(); + $matchCallback = $this->matchedRouteRule->afterMatch(); if ($matchCallback !== null) { $parameters = call_user_func($matchCallback, $parameters, $url); } @@ -83,7 +83,7 @@ public function match($url) $this->parameters = $parameters; - return $this->matchedRouteRule ? true : false; + return !is_null($this->matchedRouteRule); } /** @@ -91,7 +91,7 @@ public function match($url) * * @param string $name Name of the route previously defined. * @param array $parameters Parameter name => value items to fill in for given route. - * @return string Full matched URL as string with given values put in place of named parameters + * @return string|null Full matched URL as string with given values put in place of named parameters. Returns `null` if no route map is specified. */ public function url($name, $parameters = []) { @@ -224,7 +224,8 @@ public function getParameters() /** * Returns the matched route rule name. - * @return \Winter\Storm\Router\Rule The matched rule object. + * + * @return \Winter\Storm\Router\Rule|false The matched rule object. If no rule was matched, returns `false`. */ public function matchedRoute() { diff --git a/src/Router/Rule.php b/src/Router/Rule.php index 0c46681ad..395198499 100644 --- a/src/Router/Rule.php +++ b/src/Router/Rule.php @@ -20,12 +20,12 @@ class Rule protected $rulePattern; /** - * @var function Custom condition used when matching this rule. + * @var callable Custom condition used when matching this rule. */ protected $conditionCallback; /** - * @var function Called when this rule is matched. + * @var callable Called when this rule is matched. */ protected $afterMatchCallback; @@ -197,7 +197,7 @@ public function resolveUrl($url, &$parameters) /* * Determine if wildcard and add stored parameters as a suffix */ - if (Helper::segmentIsWildcard($patternSegment) && count($wildSegments)) { + if (Helper::segmentIsWildcard($patternSegment) && isset($wildSegments) && count($wildSegments)) { $parameters[$paramName] .= Helper::rebuildUrl($wildSegments); } } @@ -248,8 +248,10 @@ protected function captureWildcardSegments(&$urlSegments) /** * Unique route name * + * This is a getter and setter method. + * * @param string $name Unique name for the router object - * @return object Self + * @return object|string */ public function name($name = null) { @@ -265,8 +267,10 @@ public function name($name = null) /** * Route match pattern * + * This is a getter and setter method. + * * @param string $pattern Pattern used to match this rule - * @return object Self + * @return object|string */ public function pattern($pattern = null) { @@ -282,9 +286,9 @@ public function pattern($pattern = null) /** * Condition callback * - * @param callback $callback Callback function to be used when providing custom route match conditions + * @param callable $callback Callback function to be used when providing custom route match conditions * @throws InvalidArgumentException When supplied argument is not a valid callback - * @return callback + * @return callable */ public function condition($callback = null) { @@ -307,9 +311,9 @@ public function condition($callback = null) /** * After match callback * - * @param callback $callback Callback function to be used to modify params after a successful match + * @param callable $callback Callback function to be used to modify params after a successful match * @throws InvalidArgumentException When supplied argument is not a valid callback - * @return callback + * @return callable */ public function afterMatch($callback = null) { diff --git a/src/Router/UrlGenerator.php b/src/Router/UrlGenerator.php index 11a56504f..657182d13 100644 --- a/src/Router/UrlGenerator.php +++ b/src/Router/UrlGenerator.php @@ -63,12 +63,21 @@ public static function buildUrl($url, $replace = [], $flags = HTTP_URL_REPLACE, | HTTP_URL_STRIP_PASS; } + // Decode query parameters before parsing the URL + $decodeQueryParams = function (string $url): string { + if (Str::contains($url, '?')) { + list($urlWithoutQuery, $queryArgs) = explode('?', $url, 2); + $url = $urlWithoutQuery . '?' . urldecode($queryArgs); + } + return $url; + }; + // Parse input if (is_string($url)) { - $url = parse_url(urldecode($url)); + $url = parse_url($decodeQueryParams($url)); } if (is_string($replace)) { - $replace = parse_url(urldecode($replace)); + $replace = parse_url($decodeQueryParams($replace)); } // Prepare input data @@ -80,14 +89,15 @@ public static function buildUrl($url, $replace = [], $flags = HTTP_URL_REPLACE, foreach ($url as $key => &$value) { // Remove invalid segments if ( - (!in_array($key, $urlSegments) || !isset($value)) || - (is_array($value) && empty($value)) + !in_array($key, $urlSegments) + || !isset($value) + || (is_array($value) && !count($value)) ) { unset($url[$key]); continue; } - // Trim strings and remove empty strings + // Trim strings if (!is_array($value)) { $value = trim((string) $value); } @@ -162,10 +172,13 @@ public static function buildUrl($url, $replace = [], $flags = HTTP_URL_REPLACE, $rQuery = str_replace(array('[', '%5B'), '{{{', $rQuery); $rQuery = str_replace(array(']', '%5D'), '}}}', $rQuery); - parse_str($uQuery, $uQuery); - parse_str($rQuery, $rQuery); + $parsedUQuery = []; + $parsedRQuery = []; - $query = static::buildStr(array_merge($uQuery, $rQuery)); + parse_str($uQuery, $parsedUQuery); + parse_str($rQuery, $parsedRQuery); + + $query = static::buildStr(array_merge($parsedUQuery, $parsedRQuery)); $query = str_replace(array('{{{', '%7B%7B%7B'), '%5B', $query); $query = str_replace(array('}}}', '%7D%7D%7D'), '%5D', $query); @@ -270,9 +283,10 @@ public static function buildUrl($url, $replace = [], $flags = HTTP_URL_REPLACE, // Populate the query section if (isset($url['query']) && $url['query'] !== '') { + $queryParams = []; + if (is_string($url['query'])) { - $queryParams = []; - $pairs = explode(ini_get('arg_separator.output') ?? '&', $url['query']); + $pairs = explode(ini_get('arg_separator.output') ?: '&', $url['query']); foreach ($pairs as $pair) { $key = Str::before($pair, '='); $value = Str::after($pair, '='); @@ -320,7 +334,7 @@ public static function buildUrl($url, $replace = [], $flags = HTTP_URL_REPLACE, public static function buildStr(array $query, string $prefix = '', $argSeparator = null): string { if (is_null($argSeparator)) { - $argSeparator = ini_get('arg_separator.output') ?? '&'; + $argSeparator = ini_get('arg_separator.output') ?: '&'; } $result = []; diff --git a/src/Scaffold/Console/CreateCommand.php b/src/Scaffold/Console/CreateCommand.php deleted file mode 100644 index 87a4eb1bc..000000000 --- a/src/Scaffold/Console/CreateCommand.php +++ /dev/null @@ -1,84 +0,0 @@ - 'console/{{studly_name}}.php', - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - $pluginCode = $this->argument('plugin'); - - $parts = explode('.', $pluginCode); - $plugin = array_pop($parts); - $author = array_pop($parts); - $command = $this->argument('command-name'); - - return [ - 'name' => $command, - 'author' => $author, - 'plugin' => $plugin - ]; - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin. Eg: Winter.Blog'], - ['command-name', InputArgument::REQUIRED, 'The name of the command. Eg: MyCommand'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'] - ]; - } -} diff --git a/src/Scaffold/Console/CreateComponent.php b/src/Scaffold/Console/CreateComponent.php deleted file mode 100644 index dfce572af..000000000 --- a/src/Scaffold/Console/CreateComponent.php +++ /dev/null @@ -1,85 +0,0 @@ - 'components/{{studly_name}}.php', - 'component/default.stub' => 'components/{{lower_name}}/default.htm', - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - $pluginCode = $this->argument('plugin'); - - $parts = explode('.', $pluginCode); - $plugin = array_pop($parts); - $author = array_pop($parts); - $component = $this->argument('component'); - - return [ - 'name' => $component, - 'author' => $author, - 'plugin' => $plugin - ]; - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin to create. Eg: Winter.Blog'], - ['component', InputArgument::REQUIRED, 'The name of the component. Eg: Posts'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'] - ]; - } -} diff --git a/src/Scaffold/Console/CreateController.php b/src/Scaffold/Console/CreateController.php deleted file mode 100644 index 081d2ce99..000000000 --- a/src/Scaffold/Console/CreateController.php +++ /dev/null @@ -1,104 +0,0 @@ - 'controllers/{{lower_name}}/_list_toolbar.htm', - 'controller/config_form.stub' => 'controllers/{{lower_name}}/config_form.yaml', - 'controller/config_list.stub' => 'controllers/{{lower_name}}/config_list.yaml', - 'controller/create.stub' => 'controllers/{{lower_name}}/create.htm', - 'controller/index.stub' => 'controllers/{{lower_name}}/index.htm', - 'controller/preview.stub' => 'controllers/{{lower_name}}/preview.htm', - 'controller/update.stub' => 'controllers/{{lower_name}}/update.htm', - 'controller/controller.stub' => 'controllers/{{studly_name}}.php', - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - $pluginCode = $this->argument('plugin'); - - $parts = explode('.', $pluginCode); - $plugin = array_pop($parts); - $author = array_pop($parts); - - $controller = $this->argument('controller'); - - /* - * Determine the model name to use, - * either supplied or singular from the controller name. - */ - $model = $this->option('model'); - if (!$model) { - $model = Str::singular($controller); - } - - return [ - 'name' => $controller, - 'model' => $model, - 'author' => $author, - 'plugin' => $plugin - ]; - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin to create. Eg: Winter.Blog'], - ['controller', InputArgument::REQUIRED, 'The name of the controller. Eg: Posts'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'], - ['model', null, InputOption::VALUE_OPTIONAL, 'Define which model name to use, otherwise the singular controller name is used.'], - ]; - } -} diff --git a/src/Scaffold/Console/CreateFormWidget.php b/src/Scaffold/Console/CreateFormWidget.php deleted file mode 100644 index f4c0acc63..000000000 --- a/src/Scaffold/Console/CreateFormWidget.php +++ /dev/null @@ -1,88 +0,0 @@ - 'formwidgets/{{studly_name}}.php', - 'formwidget/partial.stub' => 'formwidgets/{{lower_name}}/partials/_{{lower_name}}.htm', - 'formwidget/stylesheet.stub' => 'formwidgets/{{lower_name}}/assets/css/{{lower_name}}.css', - 'formwidget/javascript.stub' => 'formwidgets/{{lower_name}}/assets/js/{{lower_name}}.js', - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - $pluginCode = $this->argument('plugin'); - - $parts = explode('.', $pluginCode); - $plugin = array_pop($parts); - $author = array_pop($parts); - - $widget = $this->argument('widget'); - - return [ - 'name' => $widget, - 'author' => $author, - 'plugin' => $plugin - ]; - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin. Eg: Winter.Blog'], - ['widget', InputArgument::REQUIRED, 'The name of the form widget. Eg: PostList'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'], - ]; - } -} diff --git a/src/Scaffold/Console/CreateModel.php b/src/Scaffold/Console/CreateModel.php deleted file mode 100644 index 6e0b54c44..000000000 --- a/src/Scaffold/Console/CreateModel.php +++ /dev/null @@ -1,88 +0,0 @@ - 'models/{{studly_name}}.php', - 'model/fields.stub' => 'models/{{lower_name}}/fields.yaml', - 'model/columns.stub' => 'models/{{lower_name}}/columns.yaml', - 'model/create_table.stub' => 'updates/create_{{snake_plural_name}}_table.php', - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - $pluginCode = $this->argument('plugin'); - - $parts = explode('.', $pluginCode); - $plugin = array_pop($parts); - $author = array_pop($parts); - - $model = $this->argument('model'); - - return [ - 'name' => $model, - 'author' => $author, - 'plugin' => $plugin - ]; - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin. Eg: Winter.Blog'], - ['model', InputArgument::REQUIRED, 'The name of the model. Eg: Post'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'], - ]; - } -} diff --git a/src/Scaffold/Console/CreatePlugin.php b/src/Scaffold/Console/CreatePlugin.php deleted file mode 100644 index 8c2821f4e..000000000 --- a/src/Scaffold/Console/CreatePlugin.php +++ /dev/null @@ -1,92 +0,0 @@ - 'Plugin.php', - 'plugin/version.stub' => 'updates/version.yaml', - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - /* - * Extract the author and name from the plugin code - */ - $pluginCode = $this->argument('plugin'); - $parts = explode('.', $pluginCode); - - if (count($parts) != 2) { - $this->error('Invalid plugin name, either too many dots or not enough.'); - $this->error('Example name: AuthorName.PluginName'); - return; - } - - - $pluginName = array_pop($parts); - $authorName = array_pop($parts); - - return [ - 'name' => $pluginName, - 'author' => $authorName, - ]; - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin to create. Eg: Winter.Blog'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'], - ]; - } -} diff --git a/src/Scaffold/Console/CreateReportWidget.php b/src/Scaffold/Console/CreateReportWidget.php deleted file mode 100644 index 8d8d7aedf..000000000 --- a/src/Scaffold/Console/CreateReportWidget.php +++ /dev/null @@ -1,86 +0,0 @@ - 'reportwidgets/{{studly_name}}.php', - 'reportwidget/widget.stub' => 'reportwidgets/{{lower_name}}/partials/_{{lower_name}}.htm', - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - $pluginCode = $this->argument('plugin'); - - $parts = explode('.', $pluginCode); - $plugin = array_pop($parts); - $author = array_pop($parts); - - $widget = $this->argument('widget'); - - return [ - 'name' => $widget, - 'author' => $author, - 'plugin' => $plugin - ]; - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin. Eg: Winter.Google'], - ['widget', InputArgument::REQUIRED, 'The name of the report widget. Eg: TopPages'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'], - ]; - } -} diff --git a/src/Scaffold/Console/CreateSettings.php b/src/Scaffold/Console/CreateSettings.php deleted file mode 100644 index a9bf99b83..000000000 --- a/src/Scaffold/Console/CreateSettings.php +++ /dev/null @@ -1,73 +0,0 @@ - 'models/{{studly_name}}.php', - 'settings/fields.stub' => 'models/{{lower_name}}/fields.yaml' - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - $pluginCode = $this->argument('plugin'); - - $parts = explode('.', $pluginCode); - $plugin = array_pop($parts); - $author = array_pop($parts); - $settings = $this->argument('settings') ?? 'Settings'; - - return [ - 'name' => $settings, - 'author' => $author, - 'plugin' => $plugin - ]; - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin. Eg: Winter.Blog'], - ['settings', InputArgument::OPTIONAL, 'The name of the settings model. Eg: Settings'], - ]; - } -} diff --git a/src/Scaffold/Console/CreateTheme.php b/src/Scaffold/Console/CreateTheme.php deleted file mode 100644 index b4c63eb5a..000000000 --- a/src/Scaffold/Console/CreateTheme.php +++ /dev/null @@ -1,138 +0,0 @@ - 'assets/js/app.js', - 'theme/assets/less/theme.stub' => 'assets/less/theme.less', - 'theme/layouts/default.stub' => 'layouts/default.htm', - 'theme/pages/404.stub' => 'pages/404.htm', - 'theme/pages/error.stub' => 'pages/error.htm', - 'theme/pages/home.stub' => 'pages/home.htm', - 'theme/partials/meta/seo.stub' => 'partials/meta/seo.htm', - 'theme/partials/meta/styles.stub' => 'partials/meta/styles.htm', - 'theme/partials/site/header.stub' => 'partials/site/header.htm', - 'theme/partials/site/footer.stub' => 'partials/site/footer.htm', - 'theme/theme.stub' => 'theme.yaml', - 'theme/version.stub' => 'version.yaml', - ]; - - /** - * Prepare variables for stubs. - * - * return @array - */ - protected function prepareVars() - { - /* - * Extract the author and name from the plugin code - */ - $code = str_slug($this->argument('theme')); - - return [ - 'code' => $code, - ]; - } - - /** - * Get the plugin path from the input. - * - * @return string - */ - protected function getDestinationPath() - { - $code = $this->prepareVars()['code']; - - return themes_path($code); - } - - /** - * Make a single stub. - * - * @param string $stubName The source filename for the stub. - */ - public function makeStub($stubName) - { - if (!isset($this->stubs[$stubName])) { - return; - } - - $sourceFile = $this->getSourcePath() . '/' . $stubName; - $destinationFile = $this->getDestinationPath() . '/' . $this->stubs[$stubName]; - $destinationContent = $this->files->get($sourceFile); - - /* - * Parse each variable in to the destination content and path - */ - foreach ($this->vars as $key => $var) { - $destinationContent = str_replace('{{' . $key . '}}', $var, $destinationContent); - $destinationFile = str_replace('{{' . $key . '}}', $var, $destinationFile); - } - - $this->makeDirectory($destinationFile); - - /* - * Make sure this file does not already exist - */ - if ($this->files->exists($destinationFile) && !$this->option('force')) { - throw new Exception('Stop everything!!! This file already exists: ' . $destinationFile); - } - - $this->files->put($destinationFile, $destinationContent); - } - - /** - * Get the console command arguments. - * - * @return array - */ - protected function getArguments() - { - return [ - ['theme', InputArgument::REQUIRED, 'The code of the theme to create. Eg: example.com'], - ]; - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'], - ]; - } -} diff --git a/src/Scaffold/Console/command/command.stub b/src/Scaffold/Console/command/command.stub deleted file mode 100644 index e681b0b96..000000000 --- a/src/Scaffold/Console/command/command.stub +++ /dev/null @@ -1,45 +0,0 @@ -output->writeln('Hello world!'); - } - - /** - * Get the console command arguments. - * @return array - */ - protected function getArguments() - { - return []; - } - - /** - * Get the console command options. - * @return array - */ - protected function getOptions() - { - return []; - } -} diff --git a/src/Scaffold/Console/component/component.stub b/src/Scaffold/Console/component/component.stub deleted file mode 100644 index 5f9bbbb7b..000000000 --- a/src/Scaffold/Console/component/component.stub +++ /dev/null @@ -1,19 +0,0 @@ - '{{name}} Component', - 'description' => 'No description provided yet...' - ]; - } - - public function defineProperties() - { - return []; - } -} diff --git a/src/Scaffold/Console/component/default.stub b/src/Scaffold/Console/component/default.stub deleted file mode 100644 index c9069b375..000000000 --- a/src/Scaffold/Console/component/default.stub +++ /dev/null @@ -1,3 +0,0 @@ -

This is the default markup for component {{name}}

- -You can delete this file if you want diff --git a/src/Scaffold/Console/controller/_list_toolbar.stub b/src/Scaffold/Console/controller/_list_toolbar.stub deleted file mode 100644 index 839d45651..000000000 --- a/src/Scaffold/Console/controller/_list_toolbar.stub +++ /dev/null @@ -1,21 +0,0 @@ -
- - New {{title_singular_name}} - - - -
diff --git a/src/Scaffold/Console/controller/config_form.stub b/src/Scaffold/Console/controller/config_form.stub deleted file mode 100644 index f5b0f4fe5..000000000 --- a/src/Scaffold/Console/controller/config_form.stub +++ /dev/null @@ -1,31 +0,0 @@ -# =================================== -# Form Behavior Config -# =================================== - -# Record name -name: {{title_singular_name}} - -# Model Form Field configuration -form: $/{{lower_author}}/{{lower_plugin}}/models/{{lower_model}}/fields.yaml - -# Model Class name -modelClass: {{studly_author}}\{{studly_plugin}}\Models\{{studly_model}} - -# Default redirect location -defaultRedirect: {{lower_author}}/{{lower_plugin}}/{{lower_name}} - -# Create page -create: - title: backend::lang.form.create_title - redirect: {{lower_author}}/{{lower_plugin}}/{{lower_name}}/update/:id - redirectClose: {{lower_author}}/{{lower_plugin}}/{{lower_name}} - -# Update page -update: - title: backend::lang.form.update_title - redirect: {{lower_author}}/{{lower_plugin}}/{{lower_name}} - redirectClose: {{lower_author}}/{{lower_plugin}}/{{lower_name}} - -# Preview page -preview: - title: backend::lang.form.preview_title diff --git a/src/Scaffold/Console/controller/config_list.stub b/src/Scaffold/Console/controller/config_list.stub deleted file mode 100644 index 6ffce9b3d..000000000 --- a/src/Scaffold/Console/controller/config_list.stub +++ /dev/null @@ -1,50 +0,0 @@ -# =================================== -# List Behavior Config -# =================================== - -# Model List Column configuration -list: $/{{lower_author}}/{{lower_plugin}}/models/{{lower_model}}/columns.yaml - -# Model Class name -modelClass: {{studly_author}}\{{studly_plugin}}\Models\{{studly_model}} - -# List Title -title: Manage {{title_plural_name}} - -# Link URL for each record -recordUrl: {{lower_author}}/{{lower_plugin}}/{{lower_name}}/update/:id - -# Message to display if the list is empty -noRecordsMessage: backend::lang.list.no_records - -# Records to display per page -recordsPerPage: 20 - -# Options to provide the user when selecting how many records to display per page -perPageOptions: [20, 40, 80, 100, 120] - -# Display page numbers with pagination, disable to improve performance -showPageNumbers: true - -# Displays the list column set up button -showSetup: true - -# Displays the sorting link on each column -showSorting: true - -# Default sorting column -# defaultSort: -# column: created_at -# direction: desc - -# Display checkboxes next to each record -showCheckboxes: true - -# Toolbar widget configuration -toolbar: - # Partial for toolbar buttons - buttons: list_toolbar - - # Search widget configuration - search: - prompt: backend::lang.list.search_prompt diff --git a/src/Scaffold/Console/controller/controller.stub b/src/Scaffold/Console/controller/controller.stub deleted file mode 100644 index 6797a76fa..000000000 --- a/src/Scaffold/Console/controller/controller.stub +++ /dev/null @@ -1,25 +0,0 @@ - - - - -fatalError): ?> - - 'layout']) ?> - -
- formRender() ?> -
- -
-
- - - - or Cancel - -
-
- - - - - -

fatalError) ?>

-

Return to {{lower_title_name}} list

- - diff --git a/src/Scaffold/Console/controller/index.stub b/src/Scaffold/Console/controller/index.stub deleted file mode 100644 index 766877d92..000000000 --- a/src/Scaffold/Console/controller/index.stub +++ /dev/null @@ -1,2 +0,0 @@ - -listRender() ?> diff --git a/src/Scaffold/Console/controller/preview.stub b/src/Scaffold/Console/controller/preview.stub deleted file mode 100644 index df5524cd4..000000000 --- a/src/Scaffold/Console/controller/preview.stub +++ /dev/null @@ -1,19 +0,0 @@ - - - - -fatalError): ?> - -
- formRenderPreview() ?> -
- - - -

fatalError) ?>

-

Return to {{lower_title_name}} list

- - diff --git a/src/Scaffold/Console/controller/update.stub b/src/Scaffold/Console/controller/update.stub deleted file mode 100644 index 7812f2b74..000000000 --- a/src/Scaffold/Console/controller/update.stub +++ /dev/null @@ -1,56 +0,0 @@ - - - - -fatalError): ?> - - 'layout']) ?> - -
- formRender() ?> -
- -
-
- - - - - or Cancel - -
-
- - - - - -

fatalError) ?>

-

Return to {{lower_title_name}} list

- - diff --git a/src/Scaffold/Console/formwidget/formwidget.stub b/src/Scaffold/Console/formwidget/formwidget.stub deleted file mode 100644 index 9fcf5045b..000000000 --- a/src/Scaffold/Console/formwidget/formwidget.stub +++ /dev/null @@ -1,57 +0,0 @@ -prepareVars(); - return $this->makePartial('{{lower_name}}'); - } - - /** - * Prepares the form widget view data - */ - public function prepareVars() - { - $this->vars['name'] = $this->formField->getName(); - $this->vars['value'] = $this->getLoadValue(); - $this->vars['model'] = $this->model; - } - - /** - * @inheritDoc - */ - public function loadAssets() - { - $this->addCss('css/{{lower_name}}.css', '{{author}}.{{plugin}}'); - $this->addJs('js/{{lower_name}}.js', '{{author}}.{{plugin}}'); - } - - /** - * @inheritDoc - */ - public function getSaveValue($value) - { - return $value; - } -} diff --git a/src/Scaffold/Console/formwidget/javascript.stub b/src/Scaffold/Console/formwidget/javascript.stub deleted file mode 100644 index d4765f0b0..000000000 --- a/src/Scaffold/Console/formwidget/javascript.stub +++ /dev/null @@ -1,5 +0,0 @@ -/* - * This is a sample JavaScript file used by {{name}} - * - * You can delete this file if you want - */ diff --git a/src/Scaffold/Console/formwidget/partial.stub b/src/Scaffold/Console/formwidget/partial.stub deleted file mode 100644 index f311f175c..000000000 --- a/src/Scaffold/Console/formwidget/partial.stub +++ /dev/null @@ -1,17 +0,0 @@ -previewMode): ?> - -
- -
- - - - - - diff --git a/src/Scaffold/Console/formwidget/stylesheet.stub b/src/Scaffold/Console/formwidget/stylesheet.stub deleted file mode 100644 index 203c17aff..000000000 --- a/src/Scaffold/Console/formwidget/stylesheet.stub +++ /dev/null @@ -1,5 +0,0 @@ -/* - * This is a sample StyleSheet file used by {{name}} - * - * You can delete this file if you want - */ diff --git a/src/Scaffold/Console/model/columns.stub b/src/Scaffold/Console/model/columns.stub deleted file mode 100644 index b11160b42..000000000 --- a/src/Scaffold/Console/model/columns.stub +++ /dev/null @@ -1,8 +0,0 @@ -# =================================== -# List Column Definitions -# =================================== - -columns: - id: - label: ID - searchable: true diff --git a/src/Scaffold/Console/model/create_table.stub b/src/Scaffold/Console/model/create_table.stub deleted file mode 100644 index 761959ed3..000000000 --- a/src/Scaffold/Console/model/create_table.stub +++ /dev/null @@ -1,22 +0,0 @@ -engine = 'InnoDB'; - $table->increments('id'); - $table->timestamps(); - }); - } - - public function down() - { - Schema::dropIfExists('{{lower_author}}_{{lower_plugin}}_{{snake_plural_name}}'); - } -} diff --git a/src/Scaffold/Console/model/fields.stub b/src/Scaffold/Console/model/fields.stub deleted file mode 100644 index c611f31c7..000000000 --- a/src/Scaffold/Console/model/fields.stub +++ /dev/null @@ -1,8 +0,0 @@ -# =================================== -# Form Field Definitions -# =================================== - -fields: - id: - label: ID - disabled: true diff --git a/src/Scaffold/Console/model/model.stub b/src/Scaffold/Console/model/model.stub deleted file mode 100644 index 0816de62b..000000000 --- a/src/Scaffold/Console/model/model.stub +++ /dev/null @@ -1,74 +0,0 @@ - '{{name}}', - 'description' => 'No description provided yet...', - 'author' => '{{author}}', - 'icon' => 'icon-leaf' - ]; - } - - /** - * Register method, called when the plugin is first registered. - * - * @return void - */ - public function register() - { - - } - - /** - * Boot method, called right before the request route. - * - * @return array - */ - public function boot() - { - - } - - /** - * Registers any front-end components implemented in this plugin. - * - * @return array - */ - public function registerComponents() - { - return []; // Remove this line to activate - - return [ - '{{studly_author}}\{{studly_name}}\Components\MyComponent' => 'myComponent', - ]; - } - - /** - * Registers any back-end permissions used by this plugin. - * - * @return array - */ - public function registerPermissions() - { - return []; // Remove this line to activate - - return [ - '{{lower_author}}.{{lower_name}}.some_permission' => [ - 'tab' => '{{name}}', - 'label' => 'Some permission', - 'roles' => [UserRole::CODE_DEVELOPER, UserRole::CODE_PUBLISHER], - ], - ]; - } - - /** - * Registers back-end navigation items for this plugin. - * - * @return array - */ - public function registerNavigation() - { - return []; // Remove this line to activate - - return [ - '{{lower_name}}' => [ - 'label' => '{{name}}', - 'url' => Backend::url('{{lower_author}}/{{lower_name}}/mycontroller'), - 'icon' => 'icon-leaf', - 'permissions' => ['{{lower_author}}.{{lower_name}}.*'], - 'order' => 500, - ], - ]; - } -} diff --git a/src/Scaffold/Console/plugin/version.stub b/src/Scaffold/Console/plugin/version.stub deleted file mode 100644 index 496b830b1..000000000 --- a/src/Scaffold/Console/plugin/version.stub +++ /dev/null @@ -1 +0,0 @@ -1.0.1: First version of {{name}} diff --git a/src/Scaffold/Console/reportwidget/reportwidget.stub b/src/Scaffold/Console/reportwidget/reportwidget.stub deleted file mode 100644 index bfe197259..000000000 --- a/src/Scaffold/Console/reportwidget/reportwidget.stub +++ /dev/null @@ -1,63 +0,0 @@ - [ - 'title' => 'backend::lang.dashboard.widget_title_label', - 'default' => '{{title_name}} Report Widget', - 'type' => 'string', - 'validationPattern' => '^.+$', - 'validationMessage' => 'backend::lang.dashboard.widget_title_error', - ], - ]; - } - - /** - * Adds widget specific asset files. Use $this->addJs() and $this->addCss() - * to register new assets to include on the page. - * @return void - */ - protected function loadAssets() - { - } - - /** - * Renders the widget's primary contents. - * @return string HTML markup supplied by this widget. - */ - public function render() - { - try { - $this->prepareVars(); - } catch (Exception $ex) { - $this->vars['error'] = $ex->getMessage(); - } - - return $this->makePartial('{{lower_name}}'); - } - - /** - * Prepares the report widget view data - */ - public function prepareVars() - { - } -} diff --git a/src/Scaffold/Console/reportwidget/widget.stub b/src/Scaffold/Console/reportwidget/widget.stub deleted file mode 100644 index 13f9152e8..000000000 --- a/src/Scaffold/Console/reportwidget/widget.stub +++ /dev/null @@ -1,9 +0,0 @@ -
-

property('title')) ?>

- - -

This is the default partial content.

- -

- -
diff --git a/src/Scaffold/Console/settings/fields.stub b/src/Scaffold/Console/settings/fields.stub deleted file mode 100644 index 7a890bd71..000000000 --- a/src/Scaffold/Console/settings/fields.stub +++ /dev/null @@ -1,7 +0,0 @@ -# =================================== -# Form Field Definitions -# =================================== - -fields: - settings_option: - label: This is a sample settings field used by {{author}}.{{plugin}} diff --git a/src/Scaffold/Console/settings/model.stub b/src/Scaffold/Console/settings/model.stub deleted file mode 100644 index 740e775a9..000000000 --- a/src/Scaffold/Console/settings/model.stub +++ /dev/null @@ -1,31 +0,0 @@ - -

Page not found

-

We're sorry, but the page you requested cannot be found.

- \ No newline at end of file diff --git a/src/Scaffold/Console/theme/pages/error.stub b/src/Scaffold/Console/theme/pages/error.stub deleted file mode 100644 index 6767bc687..000000000 --- a/src/Scaffold/Console/theme/pages/error.stub +++ /dev/null @@ -1,8 +0,0 @@ -title = "Error page (500)" -url = "/error" -layout = "default" -== -
-

Error

-

We're sorry, but something went wrong and the page cannot be displayed.

-
\ No newline at end of file diff --git a/src/Scaffold/Console/theme/pages/home.stub b/src/Scaffold/Console/theme/pages/home.stub deleted file mode 100644 index ca87de8cb..000000000 --- a/src/Scaffold/Console/theme/pages/home.stub +++ /dev/null @@ -1,7 +0,0 @@ -title = "Home" -url = "/" -layout = "default" -== -
-

Home Page

-
\ No newline at end of file diff --git a/src/Scaffold/Console/theme/partials/meta/seo.stub b/src/Scaffold/Console/theme/partials/meta/seo.stub deleted file mode 100644 index 332bb2b08..000000000 --- a/src/Scaffold/Console/theme/partials/meta/seo.stub +++ /dev/null @@ -1,11 +0,0 @@ -{% if this.theme.googleanalytics_id is not empty %} - - - -{% endif %} diff --git a/src/Scaffold/Console/theme/partials/meta/styles.stub b/src/Scaffold/Console/theme/partials/meta/styles.stub deleted file mode 100644 index 8c4144b47..000000000 --- a/src/Scaffold/Console/theme/partials/meta/styles.stub +++ /dev/null @@ -1,4 +0,0 @@ - - -{% styles %} -{% placeholder head %} diff --git a/src/Scaffold/Console/theme/partials/site/footer.stub b/src/Scaffold/Console/theme/partials/site/footer.stub deleted file mode 100644 index 565e8bd4e..000000000 --- a/src/Scaffold/Console/theme/partials/site/footer.stub +++ /dev/null @@ -1,17 +0,0 @@ - - - {% scripts %} - - {% flash %} -

- {{ message }} -

- {% endflash %} - - \ No newline at end of file diff --git a/src/Scaffold/Console/theme/partials/site/header.stub b/src/Scaffold/Console/theme/partials/site/header.stub deleted file mode 100644 index cadd5efcf..000000000 --- a/src/Scaffold/Console/theme/partials/site/header.stub +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - {% placeholder page_title default %}{{ this.page.title }}{% endplaceholder %} - {% partial "meta/styles" %} - {% partial "meta/seo" %} - - - {% set pageId = this.page.id %} - {% set pageTitle = this.page.title %} - {% if pageId is empty %} - {% set pageId = page.id %} - {% endif %} - {% if pageTitle is empty %} - {% set pageTitle = page.title %} - {% endif %} - \ No newline at end of file diff --git a/src/Scaffold/Console/theme/theme.stub b/src/Scaffold/Console/theme/theme.stub deleted file mode 100644 index ef6ca860f..000000000 --- a/src/Scaffold/Console/theme/theme.stub +++ /dev/null @@ -1,9 +0,0 @@ -name: "{{code}}" -description: "No description provided yet..." -author: "Winter CMS Scaffold" -homepage: "https://example.com" -code: "{{code}}" -form: - fields: - googleanalytics_id: - label: 'Google Analytics ID' \ No newline at end of file diff --git a/src/Scaffold/Console/theme/version.stub b/src/Scaffold/Console/theme/version.stub deleted file mode 100644 index bd1f5e6b1..000000000 --- a/src/Scaffold/Console/theme/version.stub +++ /dev/null @@ -1 +0,0 @@ -1.0.1: 'Initial version' diff --git a/src/Scaffold/GeneratorCommand.php b/src/Scaffold/GeneratorCommand.php index 6445e452e..37f2b3136 100644 --- a/src/Scaffold/GeneratorCommand.php +++ b/src/Scaffold/GeneratorCommand.php @@ -1,41 +1,118 @@ isReservedName($this->getNameInput())) { + $this->error('The name "'.$this->getNameInput().'" is reserved by PHP.'); + + return false; + } + $this->vars = $this->processVars($this->prepareVars()); $this->makeStubs(); @@ -67,25 +153,42 @@ public function handle() /** * Prepare variables for stubs. - * - * return @array */ - abstract protected function prepareVars(); + abstract protected function prepareVars(): array; /** * Make all stubs. - * - * @return void */ - public function makeStubs() + public function makeStubs(): void { $stubs = array_keys($this->stubs); + // Make sure this command won't overwrite any existing files before running + if (!$this->option('force')) { + foreach ($stubs as $stub) { + $destinationFile = $this->getDestinationForStub($stub); + if ($this->files->exists($destinationFile)) { + throw new Exception("Cannot create the {$this->type}:\r\n$destinationFile already exists.\r\nPass --force to overwrite existing files."); + } + } + } + foreach ($stubs as $stub) { $this->makeStub($stub); } } + /** + * Get the destination path for the provided stub name + */ + protected function getDestinationForStub(string $stubName): string + { + return Twig::parse( + $this->getDestinationPath() . '/' . $this->stubs[$stubName], + $this->vars + ); + } + /** * Make a single stub. * @@ -98,24 +201,16 @@ public function makeStub($stubName) } $sourceFile = $this->getSourcePath() . '/' . $stubName; - $destinationFile = $this->getDestinationPath() . '/' . $this->stubs[$stubName]; + $destinationFile = $this->getDestinationForStub($stubName); $destinationContent = $this->files->get($sourceFile); /* - * Parse each variable in to the destination content and path + * Parse each variable in to the destination content */ $destinationContent = Twig::parse($destinationContent, $this->vars); - $destinationFile = Twig::parse($destinationFile, $this->vars); $this->makeDirectory($destinationFile); - /* - * Make sure this file does not already exist - */ - if ($this->files->exists($destinationFile) && !$this->option('force')) { - throw new Exception('Stop everything!!! This file already exists: ' . $destinationFile); - } - $this->files->put($destinationFile, $destinationContent); $this->comment('File generated: ' . str_replace(base_path(), '', $destinationFile)); @@ -125,11 +220,11 @@ public function makeStub($stubName) * Build the directory for the class if necessary. * * @param string $path - * @return string + * @return void */ protected function makeDirectory($path) { - if (! $this->files->isDirectory(dirname($path))) { + if (!$this->files->isDirectory(dirname($path))) { $this->files->makeDirectory(dirname($path), 0777, true, true); } } @@ -137,11 +232,8 @@ protected function makeDirectory($path) /** * Converts all variables to available modifier and case formats. * Syntax is CASE_MODIFIER_KEY, eg: lower_plural_xxx - * - * @param array $vars The collection of original variables - * @return array A collection of variables with modifiers added */ - protected function processVars($vars) + protected function processVars(array $vars): array { $cases = ['upper', 'lower', 'snake', 'studly', 'camel', 'title']; $modifiers = ['plural', 'singular', 'title']; @@ -197,27 +289,17 @@ protected function modifyString($type, $string) } /** - * Get the plugin path from the input. - * - * @return string + * Get the base path to output generated stubs to */ - protected function getDestinationPath() + protected function getDestinationPath(): string { - $plugin = $this->getPluginInput(); - - $parts = explode('.', $plugin); - $name = array_pop($parts); - $author = array_pop($parts); - - return plugins_path(strtolower($author) . '/' . strtolower($name)); + return base_path(); } /** - * Get the source file path. - * - * @return string + * Get the base path to source stub files from */ - protected function getSourcePath() + protected function getSourcePath(): string { $className = get_class($this); $class = new ReflectionClass($className); @@ -226,36 +308,20 @@ protected function getSourcePath() } /** - * Get the desired plugin name from the input. - * - * @return string + * Get the desired class name from the input. */ - protected function getPluginInput() + protected function getNameInput(): string { - return $this->argument('plugin'); + return trim($this->argument($this->nameFrom)); } /** - * Get the console command arguments. - * - * @return array + * Checks whether the given name is reserved. */ - protected function getArguments() + protected function isReservedName(string $name): bool { - return [ - ['plugin', InputArgument::REQUIRED, 'The name of the plugin to create. Eg: Winter.Blog'], - ]; - } + $name = strtolower($name); - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return [ - ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'], - ]; + return in_array($name, $this->reservedNames); } } diff --git a/src/Scaffold/ScaffoldServiceProvider.php b/src/Scaffold/ScaffoldServiceProvider.php deleted file mode 100644 index aa8140541..000000000 --- a/src/Scaffold/ScaffoldServiceProvider.php +++ /dev/null @@ -1,77 +0,0 @@ - CreateTheme::class, - 'command.create.plugin' => CreatePlugin::class, - 'command.create.model' => CreateModel::class, - 'command.create.settings' => CreateSettings::class, - 'command.create.controller' => CreateController::class, - 'command.create.component' => CreateComponent::class, - 'command.create.formwidget' => CreateFormWidget::class, - 'command.create.reportwidget' => CreateReportWidget::class, - 'command.create.command' => CreateCommand::class, - ]; - - /** - * Register the service provider. - * - * @return void - */ - public function register() - { - if ($this->app->runningInConsole()) { - $this->commands( - [ - 'command.create.theme', - 'command.create.plugin', - 'command.create.model', - 'command.create.settings', - 'command.create.controller', - 'command.create.component', - 'command.create.formwidget', - 'command.create.reportwidget', - 'command.create.command', - ] - ); - } - } - - /** - * Get the services provided by the provider. - * - * @return array - */ - public function provides() - { - return [ - 'command.create.theme', - 'command.create.plugin', - 'command.create.model', - 'command.create.settings', - 'command.create.controller', - 'command.create.component', - 'command.create.formwidget', - 'command.create.reportwidget', - 'command.create.command', - ]; - } -} diff --git a/src/Support/Arr.php b/src/Support/Arr.php index 7af23d6f0..ea94efd45 100644 --- a/src/Support/Arr.php +++ b/src/Support/Arr.php @@ -28,23 +28,4 @@ public static function build(array $array, callable $callback) return $results; } - - /** - * Transform a dot-notated array into a normal array. - * - * Courtesy of https://github.com/laravel/framework/issues/1851#issuecomment-20796924 - * - * @param array $dotArray - * @return array - */ - public static function undot(array $dotArray) - { - $array = []; - - foreach ($dotArray as $key => $value) { - static::set($array, $key, $value); - } - - return $array; - } } diff --git a/src/Support/ClassLoader.php b/src/Support/ClassLoader.php index da80d04f0..f9ffd932d 100644 --- a/src/Support/ClassLoader.php +++ b/src/Support/ClassLoader.php @@ -1,8 +1,8 @@ registered) { + if (!is_null($this->registered)) { return; } $this->ensureManifestIsLoaded(); - $this->registered = spl_autoload_register([$this, 'load']); + $this->registered = function ($class) { + $this->load($class); + }; + spl_autoload_register($this->registered); } /** @@ -218,12 +221,12 @@ public function register() */ public function unregister() { - if (!$this->registered) { + if (is_null($this->registered)) { return; } - spl_autoload_unregister([$this, 'load']); - $this->registered = false; + spl_autoload_unregister($this->registered); + $this->registered = null; } /** @@ -309,7 +312,7 @@ public function addAliases(array $aliases) * Aliases are first-come, first-served. If a real class already exists with the same name as an alias, the real * class is used over the alias. * - * @param array $aliases + * @param array $namespaceAliases * @return void */ public function addNamespaceAliases(array $namespaceAliases) @@ -409,7 +412,7 @@ protected static function normalizeClass($class) * Get the possible paths for a class. * * @param string $class - * @return string + * @return array */ protected static function getPathsForClass($class) { diff --git a/src/Support/Facade.php b/src/Support/Facade.php index 84b05071a..ec68bd149 100644 --- a/src/Support/Facade.php +++ b/src/Support/Facade.php @@ -10,31 +10,4 @@ */ class Facade extends FacadeParent { - - /** - * @inheritDoc - */ - protected static function resolveFacadeInstance($name) - { - if ( - !is_object($name) && - !is_null(static::$app) && - !static::$app->bound($name) && - ($instance = static::getFacadeInstance()) !== null - ) { - static::$app->instance($name, $instance); - } - - return parent::resolveFacadeInstance($name); - } - - /** - * If the accessor is not found via getFacadeAccessor, use this instance as a fallback. - * - * @return mixed - */ - protected static function getFacadeInstance() - { - return null; - } } diff --git a/src/Support/Facades/DB.php b/src/Support/Facades/DB.php new file mode 100644 index 000000000..f5738678c --- /dev/null +++ b/src/Support/Facades/DB.php @@ -0,0 +1,50 @@ +input($key, $default); } + /** + * Gets all input data items. + * + * This method is used for all request verbs (GET, POST, PUT, and DELETE) + * + * @return array|null + */ + public static function all() + { + return static::$app['request']->input(); + } + /** * Get the registered name of the component. * diff --git a/src/Support/Facades/Schema.php b/src/Support/Facades/Schema.php index 491ab68fd..b3c38ce20 100644 --- a/src/Support/Facades/Schema.php +++ b/src/Support/Facades/Schema.php @@ -20,6 +20,13 @@ */ class Schema extends Facade { + /** + * Indicates if the resolved facade should be cached. + * + * @var bool + */ + protected static $cached = false; + /** * Get a schema builder instance for a connection. * @@ -38,14 +45,10 @@ public static function connection($name) /** * Get a schema builder instance for the default connection. * - * @return \Illuminate\Database\Schema\Builder + * @return string */ protected static function getFacadeAccessor() { - $builder = static::$app['db']->connection()->getSchemaBuilder(); - - static::$app['events']->fire('db.schema.getBuilder', [$builder]); - - return $builder; + return 'db.schema'; } } diff --git a/src/Support/Facades/Str.php b/src/Support/Facades/Str.php deleted file mode 100644 index 5bcb93970..000000000 --- a/src/Support/Facades/Str.php +++ /dev/null @@ -1,55 +0,0 @@ -loadViewsFrom($modulePath . '/views', $module); $this->loadTranslationsFrom($modulePath . '/lang', $module); $this->loadConfigFrom($modulePath . '/config', $module); - } - } - /** - * Register the service provider. - * @return void - */ - public function register() - { - if ($module = $this->getModule(func_get_args())) { /* * Add routes, if available */ $routesFile = base_path() . '/modules/' . $module . '/routes.php'; if (File::isFile($routesFile)) { - require $routesFile; + $this->loadRoutesFrom($routesFile); } } } @@ -55,8 +51,8 @@ public function getModule($args) /** * Registers a new console (artisan) command - * @param $key The command name - * @param $class The command class + * @param string $key The command name + * @param string $class The command class * @return void */ public function registerConsoleCommand($key, $class) diff --git a/src/Support/Serialization.php b/src/Support/Serialization.php new file mode 100644 index 000000000..54587bc71 --- /dev/null +++ b/src/Support/Serialization.php @@ -0,0 +1,40 @@ +getClosure(); + } + return $callable; + } +} diff --git a/src/Support/Singleton.php b/src/Support/Singleton.php index f4452c71d..5dc5b9ace 100644 --- a/src/Support/Singleton.php +++ b/src/Support/Singleton.php @@ -1,6 +1,6 @@ buildMailable($view, $data, $callback, true); } - return parent::queue($view, $data = null, $callback = null, $queue = null); + return parent::queue($view, $queue = null); } /** diff --git a/src/Support/Traits/Emitter.php b/src/Support/Traits/Emitter.php index c6629e628..d52e14b93 100644 --- a/src/Support/Traits/Emitter.php +++ b/src/Support/Traits/Emitter.php @@ -1,5 +1,11 @@ emitterEventCollection[$event][$priority][] = $callback; + if ($event instanceof Closure || $event instanceof QueuedClosure) { + if ($priority === 0 && (is_int($callback) || filter_var($callback, FILTER_VALIDATE_INT))) { + $priority = (int) $callback; + } + } + if ($event instanceof Closure) { + return $this->bindEvent($this->firstClosureParameterType($event), $event, $priority); + } elseif ($event instanceof QueuedClosure) { + return $this->bindEvent($this->firstClosureParameterType($event->closure), $event->resolve(), $priority); + } elseif ($callback instanceof QueuedClosure) { + $callback = $callback->resolve(); + } + $this->emitterEventCollection[$event][$priority][] = Serialization::wrapClosure($callback); unset($this->emitterEventSorted[$event]); return $this; } /** * Create a new event binding that fires once only + * @param string|Closure|QueuedClosure $event + * @param QueuedClosure|Closure|null $callback When a Closure or QueuedClosure is provided as the first parameter + * this parameter can be omitted * @return self */ - public function bindEventOnce($event, $callback) + public function bindEventOnce($event, $callback = null) { - $this->emitterSingleEventCollection[$event][] = $callback; + if ($event instanceof Closure) { + return $this->bindEventOnce($this->firstClosureParameterType($event), $event); + } elseif ($event instanceof QueuedClosure) { + return $this->bindEventOnce($this->firstClosureParameterType($event->closure), $event->resolve()); + } elseif ($callback instanceof QueuedClosure) { + $callback = $callback->resolve(); + } + $this->emitterSingleEventCollection[$event][] = Serialization::wrapClosure($callback); return $this; } @@ -47,7 +81,7 @@ public function bindEventOnce($event, $callback) * Sort the listeners for a given event by priority. * * @param string $eventName - * @return array + * @return void */ protected function emitterEventSortEvents($eventName) { @@ -62,7 +96,7 @@ protected function emitterEventSortEvents($eventName) /** * Destroys an event binding. - * @param string $event Event to destroy + * @param string|array|object $event Event to destroy * @return self */ public function unbindEvent($event = null) @@ -74,7 +108,11 @@ public function unbindEvent($event = null) foreach ($event as $_event) { $this->unbindEvent($_event); } - return; + return $this; + } + + if (is_object($event)) { + $event = get_class($event); } if ($event === null) { @@ -102,13 +140,16 @@ public function unbindEvent($event = null) * @param string $event Event name * @param array $params Event parameters * @param boolean $halt Halt after first non-null result - * @return array Collection of event results / Or single result (if halted) + * @return array|mixed|null If halted, the first non-null result. If not halted, an array of event results. Returns + * null if no listeners returned a result. */ public function fireEvent($event, $params = [], $halt = false) { - if (!is_array($params)) { - $params = [$params]; - } + // When the given "event" is actually an object we will assume it is an event + // object and use the class as the event name and this event itself as the + // payload to the handler, which makes object based events quite simple. + list($event, $params) = $this->parseEventAndPayload($event, $params); + $result = []; /* @@ -116,7 +157,7 @@ public function fireEvent($event, $params = [], $halt = false) */ if (isset($this->emitterSingleEventCollection[$event])) { foreach ($this->emitterSingleEventCollection[$event] as $callback) { - $response = call_user_func_array($callback, $params); + $response = call_user_func_array(Serialization::unwrapClosure($callback), $params); if (is_null($response)) { continue; } @@ -138,7 +179,7 @@ public function fireEvent($event, $params = [], $halt = false) } foreach ($this->emitterEventSorted[$event] as $callback) { - $response = call_user_func_array($callback, $params); + $response = call_user_func_array(Serialization::unwrapClosure($callback), $params); if (is_null($response)) { continue; } @@ -151,4 +192,20 @@ public function fireEvent($event, $params = [], $halt = false) return $halt ? null : $result; } + + /** + * Parse the given event and payload and prepare them for dispatching. + * + * @param mixed $event + * @param mixed $payload + * @return array + */ + protected function parseEventAndPayload($event, $payload = null) + { + if (is_object($event)) { + [$payload, $event] = [[$event], get_class($event)]; + } + + return [$event, Arr::wrap($payload)]; + } } diff --git a/src/Support/Traits/Singleton.php b/src/Support/Traits/Singleton.php index 1da6e74f3..79d0bb6cd 100644 --- a/src/Support/Traits/Singleton.php +++ b/src/Support/Traits/Singleton.php @@ -14,6 +14,8 @@ trait Singleton /** * Create a new instance of this singleton. + * + * @return static */ final public static function instance() { diff --git a/src/Support/aliases.php b/src/Support/aliases.php index 9dccbe6f9..a3debf09e 100644 --- a/src/Support/aliases.php +++ b/src/Support/aliases.php @@ -15,73 +15,66 @@ class_alias(\Winter\Storm\Argon\ArgonServiceProvider::class, \October\Rain\Argon /** * Alias October\Rain\Assetic */ -class_alias(\Winter\Storm\Assetic\Asset\AssetCache::class, \October\Rain\Assetic\Asset\AssetCache::class); -class_alias(\Winter\Storm\Assetic\Asset\AssetCollection::class, \October\Rain\Assetic\Asset\AssetCollection::class); -class_alias(\Winter\Storm\Assetic\Asset\AssetCollectionInterface::class, \October\Rain\Assetic\Asset\AssetCollectionInterface::class); -class_alias(\Winter\Storm\Assetic\Asset\AssetInterface::class, \October\Rain\Assetic\Asset\AssetInterface::class); -class_alias(\Winter\Storm\Assetic\Asset\AssetReference::class, \October\Rain\Assetic\Asset\AssetReference::class); -class_alias(\Winter\Storm\Assetic\Asset\BaseAsset::class, \October\Rain\Assetic\Asset\BaseAsset::class); -class_alias(\Winter\Storm\Assetic\Asset\FileAsset::class, \October\Rain\Assetic\Asset\FileAsset::class); -class_alias(\Winter\Storm\Assetic\Asset\GlobAsset::class, \October\Rain\Assetic\Asset\GlobAsset::class); -class_alias(\Winter\Storm\Assetic\Asset\HttpAsset::class, \October\Rain\Assetic\Asset\HttpAsset::class); -class_alias(\Winter\Storm\Assetic\Asset\Iterator\AssetCollectionFilterIterator::class, \October\Rain\Assetic\Asset\Iterator\AssetCollectionFilterIterator::class); -class_alias(\Winter\Storm\Assetic\Asset\Iterator\AssetCollectionIterator::class, \October\Rain\Assetic\Asset\Iterator\AssetCollectionIterator::class); -class_alias(\Winter\Storm\Assetic\Asset\StringAsset::class, \October\Rain\Assetic\Asset\StringAsset::class); -class_alias(\Winter\Storm\Assetic\AssetManager::class, \October\Rain\Assetic\AssetManager::class); -class_alias(\Winter\Storm\Assetic\AssetWriter::class, \October\Rain\Assetic\AssetWriter::class); -class_alias(\Winter\Storm\Assetic\Cache\ApcCache::class, \October\Rain\Assetic\Cache\ApcCache::class); -class_alias(\Winter\Storm\Assetic\Cache\ArrayCache::class, \October\Rain\Assetic\Cache\ArrayCache::class); -class_alias(\Winter\Storm\Assetic\Cache\CacheInterface::class, \October\Rain\Assetic\Cache\CacheInterface::class); -class_alias(\Winter\Storm\Assetic\Cache\ConfigCache::class, \October\Rain\Assetic\Cache\ConfigCache::class); -class_alias(\Winter\Storm\Assetic\Cache\ExpiringCache::class, \October\Rain\Assetic\Cache\ExpiringCache::class); -class_alias(\Winter\Storm\Assetic\Cache\FilesystemCache::class, \October\Rain\Assetic\Cache\FilesystemCache::class); -class_alias(\Winter\Storm\Assetic\Exception\Exception::class, \October\Rain\Assetic\Exception\Exception::class); -class_alias(\Winter\Storm\Assetic\Exception\FilterException::class, \October\Rain\Assetic\Exception\FilterException::class); -class_alias(\Winter\Storm\Assetic\Factory\AssetFactory::class, \October\Rain\Assetic\Factory\AssetFactory::class); -class_alias(\Winter\Storm\Assetic\Factory\LazyAssetManager::class, \October\Rain\Assetic\Factory\LazyAssetManager::class); -class_alias(\Winter\Storm\Assetic\Factory\Loader\BasePhpFormulaLoader::class, \October\Rain\Assetic\Factory\Loader\BasePhpFormulaLoader::class); -class_alias(\Winter\Storm\Assetic\Factory\Loader\CachedFormulaLoader::class, \October\Rain\Assetic\Factory\Loader\CachedFormulaLoader::class); -class_alias(\Winter\Storm\Assetic\Factory\Loader\FormulaLoaderInterface::class, \October\Rain\Assetic\Factory\Loader\FormulaLoaderInterface::class); -class_alias(\Winter\Storm\Assetic\Factory\Loader\FunctionCallsFormulaLoader::class, \October\Rain\Assetic\Factory\Loader\FunctionCallsFormulaLoader::class); -class_alias(\Winter\Storm\Assetic\Factory\Resource\CoalescingDirectoryResource::class, \October\Rain\Assetic\Factory\Resource\CoalescingDirectoryResource::class); -class_alias(\Winter\Storm\Assetic\Factory\Resource\DirectoryResource::class, \October\Rain\Assetic\Factory\Resource\DirectoryResource::class); -class_alias(\Winter\Storm\Assetic\Factory\Resource\DirectoryResourceFilterIterator::class, \October\Rain\Assetic\Factory\Resource\DirectoryResourceFilterIterator::class); -class_alias(\Winter\Storm\Assetic\Factory\Resource\DirectoryResourceIterator::class, \October\Rain\Assetic\Factory\Resource\DirectoryResourceIterator::class); -class_alias(\Winter\Storm\Assetic\Factory\Resource\FileResource::class, \October\Rain\Assetic\Factory\Resource\FileResource::class); -class_alias(\Winter\Storm\Assetic\Factory\Resource\IteratorResourceInterface::class, \October\Rain\Assetic\Factory\Resource\IteratorResourceInterface::class); -class_alias(\Winter\Storm\Assetic\Factory\Resource\ResourceInterface::class, \October\Rain\Assetic\Factory\Resource\ResourceInterface::class); -class_alias(\Winter\Storm\Assetic\Factory\Worker\CacheBustingWorker::class, \October\Rain\Assetic\Factory\Worker\CacheBustingWorker::class); -class_alias(\Winter\Storm\Assetic\Factory\Worker\EnsureFilterWorker::class, \October\Rain\Assetic\Factory\Worker\EnsureFilterWorker::class); -class_alias(\Winter\Storm\Assetic\Factory\Worker\WorkerInterface::class, \October\Rain\Assetic\Factory\Worker\WorkerInterface::class); -class_alias(\Winter\Storm\Assetic\Filter\BaseCssFilter::class, \October\Rain\Assetic\Filter\BaseCssFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\CallablesFilter::class, \October\Rain\Assetic\Filter\CallablesFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\CssCacheBustingFilter::class, \October\Rain\Assetic\Filter\CssCacheBustingFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\CssImportFilter::class, \October\Rain\Assetic\Filter\CssImportFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\CssMinFilter::class, \October\Rain\Assetic\Filter\CssMinFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\CssRewriteFilter::class, \October\Rain\Assetic\Filter\CssRewriteFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\DependencyExtractorInterface::class, \October\Rain\Assetic\Filter\DependencyExtractorInterface::class); -class_alias(\Winter\Storm\Assetic\Filter\FilterCollection::class, \October\Rain\Assetic\Filter\FilterCollection::class); -class_alias(\Winter\Storm\Assetic\Filter\FilterInterface::class, \October\Rain\Assetic\Filter\FilterInterface::class); -class_alias(\Winter\Storm\Assetic\Filter\HashableInterface::class, \October\Rain\Assetic\Filter\HashableInterface::class); -class_alias(\Winter\Storm\Assetic\Filter\JavascriptImporter::class, \October\Rain\Assetic\Filter\JavascriptImporter::class); -class_alias(\Winter\Storm\Assetic\Filter\JSMinFilter::class, \October\Rain\Assetic\Filter\JSMinFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\JSMinPlusFilter::class, \October\Rain\Assetic\Filter\JSMinPlusFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\JSqueezeFilter::class, \October\Rain\Assetic\Filter\JSqueezeFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\LessCompiler::class, \October\Rain\Assetic\Filter\LessCompiler::class); -class_alias(\Winter\Storm\Assetic\Filter\LessphpFilter::class, \October\Rain\Assetic\Filter\LessphpFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\MinifyCssCompressorFilter::class, \October\Rain\Assetic\Filter\MinifyCssCompressorFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\PackagerFilter::class, \October\Rain\Assetic\Filter\PackagerFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\PackerFilter::class, \October\Rain\Assetic\Filter\PackerFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\ScssCompiler::class, \October\Rain\Assetic\Filter\ScssCompiler::class); -class_alias(\Winter\Storm\Assetic\Filter\ScssphpFilter::class, \October\Rain\Assetic\Filter\ScssphpFilter::class); -class_alias(\Winter\Storm\Assetic\Filter\StylesheetMinify::class, \October\Rain\Assetic\Filter\StylesheetMinify::class); -class_alias(\Winter\Storm\Assetic\FilterManager::class, \October\Rain\Assetic\FilterManager::class); -class_alias(\Winter\Storm\Assetic\Util\CssUtils::class, \October\Rain\Assetic\Util\CssUtils::class); -class_alias(\Winter\Storm\Assetic\Util\FilesystemUtils::class, \October\Rain\Assetic\Util\FilesystemUtils::class); -class_alias(\Winter\Storm\Assetic\Util\LessUtils::class, \October\Rain\Assetic\Util\LessUtils::class); -class_alias(\Winter\Storm\Assetic\Util\SassUtils::class, \October\Rain\Assetic\Util\SassUtils::class); -class_alias(\Winter\Storm\Assetic\Util\TraversableString::class, \October\Rain\Assetic\Util\TraversableString::class); -class_alias(\Winter\Storm\Assetic\Util\VarUtils::class, \October\Rain\Assetic\Util\VarUtils::class); +class_alias(\Assetic\Asset\AssetCache::class, \October\Rain\Assetic\Asset\AssetCache::class); +class_alias(\Assetic\Asset\AssetCollection::class, \October\Rain\Assetic\Asset\AssetCollection::class); +class_alias(\Assetic\Contracts\Asset\AssetCollectionInterface::class, \October\Rain\Assetic\Asset\AssetCollectionInterface::class); +class_alias(\Assetic\Contracts\Asset\AssetInterface::class, \October\Rain\Assetic\Asset\AssetInterface::class); +class_alias(\Assetic\Asset\AssetReference::class, \October\Rain\Assetic\Asset\AssetReference::class); +class_alias(\Assetic\Asset\BaseAsset::class, \October\Rain\Assetic\Asset\BaseAsset::class); +class_alias(\Assetic\Asset\FileAsset::class, \October\Rain\Assetic\Asset\FileAsset::class); +class_alias(\Assetic\Asset\GlobAsset::class, \October\Rain\Assetic\Asset\GlobAsset::class); +class_alias(\Assetic\Asset\HttpAsset::class, \October\Rain\Assetic\Asset\HttpAsset::class); +class_alias(\Assetic\Asset\Iterator\AssetCollectionFilterIterator::class, \October\Rain\Assetic\Asset\Iterator\AssetCollectionFilterIterator::class); +class_alias(\Assetic\Asset\Iterator\AssetCollectionIterator::class, \October\Rain\Assetic\Asset\Iterator\AssetCollectionIterator::class); +class_alias(\Assetic\Asset\StringAsset::class, \October\Rain\Assetic\Asset\StringAsset::class); +class_alias(\Assetic\AssetManager::class, \October\Rain\Assetic\AssetManager::class); +class_alias(\Assetic\AssetWriter::class, \October\Rain\Assetic\AssetWriter::class); +class_alias(\Assetic\Cache\ArrayCache::class, \October\Rain\Assetic\Cache\ArrayCache::class); +class_alias(\Assetic\Contracts\Cache\CacheInterface::class, \October\Rain\Assetic\Cache\CacheInterface::class); +class_alias(\Assetic\Cache\ConfigCache::class, \October\Rain\Assetic\Cache\ConfigCache::class); +class_alias(\Assetic\Cache\ExpiringCache::class, \October\Rain\Assetic\Cache\ExpiringCache::class); +class_alias(\Winter\Storm\Parse\Assetic\Cache\FilesystemCache::class, \October\Rain\Assetic\Cache\FilesystemCache::class); +class_alias(\Assetic\Contracts\Exception\Exception::class, \October\Rain\Assetic\Exception\Exception::class); +class_alias(\Assetic\Exception\FilterException::class, \October\Rain\Assetic\Exception\FilterException::class); +class_alias(\Assetic\Factory\AssetFactory::class, \October\Rain\Assetic\Factory\AssetFactory::class); +class_alias(\Assetic\Factory\LazyAssetManager::class, \October\Rain\Assetic\Factory\LazyAssetManager::class); +class_alias(\Assetic\Factory\Loader\BasePhpFormulaLoader::class, \October\Rain\Assetic\Factory\Loader\BasePhpFormulaLoader::class); +class_alias(\Assetic\Factory\Loader\CachedFormulaLoader::class, \October\Rain\Assetic\Factory\Loader\CachedFormulaLoader::class); +class_alias(\Assetic\Contracts\Factory\Loader\FormulaLoaderInterface::class, \October\Rain\Assetic\Factory\Loader\FormulaLoaderInterface::class); +class_alias(\Assetic\Factory\Loader\FunctionCallsFormulaLoader::class, \October\Rain\Assetic\Factory\Loader\FunctionCallsFormulaLoader::class); +class_alias(\Assetic\Factory\Resource\CoalescingDirectoryResource::class, \October\Rain\Assetic\Factory\Resource\CoalescingDirectoryResource::class); +class_alias(\Assetic\Factory\Resource\DirectoryResource::class, \October\Rain\Assetic\Factory\Resource\DirectoryResource::class); +class_alias(\Assetic\Factory\Resource\DirectoryResourceFilterIterator::class, \October\Rain\Assetic\Factory\Resource\DirectoryResourceFilterIterator::class); +class_alias(\Assetic\Factory\Resource\DirectoryResourceIterator::class, \October\Rain\Assetic\Factory\Resource\DirectoryResourceIterator::class); +class_alias(\Assetic\Factory\Resource\FileResource::class, \October\Rain\Assetic\Factory\Resource\FileResource::class); +class_alias(\Assetic\Contracts\Factory\Resource\IteratorResourceInterface::class, \October\Rain\Assetic\Factory\Resource\IteratorResourceInterface::class); +class_alias(\Assetic\Contracts\Factory\Resource\ResourceInterface::class, \October\Rain\Assetic\Factory\Resource\ResourceInterface::class); +class_alias(\Assetic\Factory\Worker\CacheBustingWorker::class, \October\Rain\Assetic\Factory\Worker\CacheBustingWorker::class); +class_alias(\Assetic\Factory\Worker\EnsureFilterWorker::class, \October\Rain\Assetic\Factory\Worker\EnsureFilterWorker::class); +class_alias(\Assetic\Contracts\Factory\Worker\WorkerInterface::class, \October\Rain\Assetic\Factory\Worker\WorkerInterface::class); +class_alias(\Assetic\Filter\BaseCssFilter::class, \October\Rain\Assetic\Filter\BaseCssFilter::class); +class_alias(\Assetic\Filter\CallablesFilter::class, \October\Rain\Assetic\Filter\CallablesFilter::class); +class_alias(\Assetic\Filter\CssCacheBustingFilter::class, \October\Rain\Assetic\Filter\CssCacheBustingFilter::class); +class_alias(\Assetic\Filter\CssImportFilter::class, \October\Rain\Assetic\Filter\CssImportFilter::class); +class_alias(\Assetic\Filter\CssRewriteFilter::class, \October\Rain\Assetic\Filter\CssRewriteFilter::class); +class_alias(\Assetic\Contracts\Filter\DependencyExtractorInterface::class, \October\Rain\Assetic\Filter\DependencyExtractorInterface::class); +class_alias(\Assetic\Filter\FilterCollection::class, \October\Rain\Assetic\Filter\FilterCollection::class); +class_alias(\Assetic\Contracts\Filter\FilterInterface::class, \October\Rain\Assetic\Filter\FilterInterface::class); +class_alias(\Assetic\Contracts\Filter\HashableInterface::class, \October\Rain\Assetic\Filter\HashableInterface::class); +class_alias(\Winter\Storm\Parse\Assetic\Filter\JavascriptImporter::class, \October\Rain\Assetic\Filter\JavascriptImporter::class); +class_alias(\Winter\Storm\Parse\Assetic\Filter\LessCompiler::class, \October\Rain\Assetic\Filter\LessCompiler::class); +class_alias(\Assetic\Filter\LessphpFilter::class, \October\Rain\Assetic\Filter\LessphpFilter::class); +class_alias(\Assetic\Filter\PackerFilter::class, \October\Rain\Assetic\Filter\PackerFilter::class); +class_alias(\Winter\Storm\Parse\Assetic\Filter\ScssCompiler::class, \October\Rain\Assetic\Filter\ScssCompiler::class); +class_alias(\Assetic\Filter\ScssphpFilter::class, \October\Rain\Assetic\Filter\ScssphpFilter::class); +class_alias(\Assetic\Filter\StylesheetMinifyFilter::class, \October\Rain\Assetic\Filter\StylesheetMinify::class); +class_alias(\Assetic\FilterManager::class, \October\Rain\Assetic\FilterManager::class); +class_alias(\Assetic\Util\CssUtils::class, \October\Rain\Assetic\Util\CssUtils::class); +class_alias(\Assetic\Util\FilesystemUtils::class, \October\Rain\Assetic\Util\FilesystemUtils::class); +class_alias(\Assetic\Util\LessUtils::class, \October\Rain\Assetic\Util\LessUtils::class); +class_alias(\Assetic\Util\SassUtils::class, \October\Rain\Assetic\Util\SassUtils::class); +class_alias(\Assetic\Util\TraversableString::class, \October\Rain\Assetic\Util\TraversableString::class); +class_alias(\Assetic\Util\VarUtils::class, \October\Rain\Assetic\Util\VarUtils::class); /** * Alias October\Rain\Auth @@ -106,7 +99,6 @@ class_alias(\Winter\Storm\Config\Repository::class, \October\Rain\Config\Reposit /** * Alias October\Rain\Cookie */ -class_alias(\Winter\Storm\Cookie\CookieValuePrefix::class, \October\Rain\Cookie\CookieValuePrefix::class); class_alias(\Winter\Storm\Cookie\Middleware\EncryptCookies::class, \October\Rain\Cookie\Middleware\EncryptCookies::class); /** @@ -148,19 +140,19 @@ class_alias(\Winter\Storm\Database\Query\Grammars\SqlServerGrammar::class, \Octo class_alias(\Winter\Storm\Database\QueryBuilder::class, \October\Rain\Database\QueryBuilder::class); class_alias(\Winter\Storm\Database\Relations\AttachMany::class, \October\Rain\Database\Relations\AttachMany::class); class_alias(\Winter\Storm\Database\Relations\AttachOne::class, \October\Rain\Database\Relations\AttachOne::class); -class_alias(\Winter\Storm\Database\Relations\AttachOneOrMany::class, \October\Rain\Database\Relations\AttachOneOrMany::class); +class_alias(\Winter\Storm\Database\Relations\Concerns\AttachOneOrMany::class, \October\Rain\Database\Relations\AttachOneOrMany::class); class_alias(\Winter\Storm\Database\Relations\BelongsTo::class, \October\Rain\Database\Relations\BelongsTo::class); class_alias(\Winter\Storm\Database\Relations\BelongsToMany::class, \October\Rain\Database\Relations\BelongsToMany::class); -class_alias(\Winter\Storm\Database\Relations\DeferOneOrMany::class, \October\Rain\Database\Relations\DeferOneOrMany::class); -class_alias(\Winter\Storm\Database\Relations\DefinedConstraints::class, \October\Rain\Database\Relations\DefinedConstraints::class); +class_alias(\Winter\Storm\Database\Relations\Concerns\DeferOneOrMany::class, \October\Rain\Database\Relations\DeferOneOrMany::class); +class_alias(\Winter\Storm\Database\Relations\Concerns\DefinedConstraints::class, \October\Rain\Database\Relations\DefinedConstraints::class); class_alias(\Winter\Storm\Database\Relations\HasMany::class, \October\Rain\Database\Relations\HasMany::class); class_alias(\Winter\Storm\Database\Relations\HasManyThrough::class, \October\Rain\Database\Relations\HasManyThrough::class); class_alias(\Winter\Storm\Database\Relations\HasOne::class, \October\Rain\Database\Relations\HasOne::class); -class_alias(\Winter\Storm\Database\Relations\HasOneOrMany::class, \October\Rain\Database\Relations\HasOneOrMany::class); +class_alias(\Winter\Storm\Database\Relations\Concerns\HasOneOrMany::class, \October\Rain\Database\Relations\HasOneOrMany::class); class_alias(\Winter\Storm\Database\Relations\HasOneThrough::class, \October\Rain\Database\Relations\HasOneThrough::class); class_alias(\Winter\Storm\Database\Relations\MorphMany::class, \October\Rain\Database\Relations\MorphMany::class); class_alias(\Winter\Storm\Database\Relations\MorphOne::class, \October\Rain\Database\Relations\MorphOne::class); -class_alias(\Winter\Storm\Database\Relations\MorphOneOrMany::class, \October\Rain\Database\Relations\MorphOneOrMany::class); +class_alias(\Winter\Storm\Database\Relations\Concerns\MorphOneOrMany::class, \October\Rain\Database\Relations\MorphOneOrMany::class); class_alias(\Winter\Storm\Database\Relations\MorphTo::class, \October\Rain\Database\Relations\MorphTo::class); class_alias(\Winter\Storm\Database\Relations\MorphToMany::class, \October\Rain\Database\Relations\MorphToMany::class); class_alias(\Winter\Storm\Database\Relations\Relation::class, \October\Rain\Database\Relations\Relation::class); @@ -214,7 +206,7 @@ class_alias(\Winter\Storm\Extension\ExtensionTrait::class, \October\Rain\Extensi */ class_alias(\Winter\Storm\Filesystem\Definitions::class, \October\Rain\Filesystem\Definitions::class); class_alias(\Winter\Storm\Filesystem\Filesystem::class, \October\Rain\Filesystem\Filesystem::class); -class_alias(\Winter\Storm\Filesystem\FilesystemAdapter::class, \October\Rain\Filesystem\FilesystemAdapter::class); +class_alias(\Illuminate\Filesystem\FilesystemAdapter::class, \October\Rain\Filesystem\FilesystemAdapter::class); class_alias(\Winter\Storm\Filesystem\FilesystemManager::class, \October\Rain\Filesystem\FilesystemManager::class); class_alias(\Winter\Storm\Filesystem\FilesystemServiceProvider::class, \October\Rain\Filesystem\FilesystemServiceProvider::class); class_alias(\Winter\Storm\Filesystem\PathResolver::class, \October\Rain\Filesystem\PathResolver::class); @@ -298,9 +290,6 @@ class_alias(\Winter\Storm\Mail\Mailable::class, \October\Rain\Mail\Mailable::cla class_alias(\Winter\Storm\Mail\Mailer::class, \October\Rain\Mail\Mailer::class); class_alias(\Winter\Storm\Mail\MailParser::class, \October\Rain\Mail\MailParser::class); class_alias(\Winter\Storm\Mail\MailServiceProvider::class, \October\Rain\Mail\MailServiceProvider::class); -class_alias(\Winter\Storm\Mail\Transport\MandrillTransport::class, \October\Rain\Mail\Transport\MandrillTransport::class); -class_alias(\Winter\Storm\Mail\Transport\SparkPostTransport::class, \October\Rain\Mail\Transport\SparkPostTransport::class); -class_alias(\Winter\Storm\Mail\TransportManager::class, \October\Rain\Mail\TransportManager::class); /** * Alias October\Rain\Network @@ -341,16 +330,7 @@ class_alias(\Winter\Storm\Router\UrlGenerator::class, \October\Rain\Router\UrlGe /** * Alias October\Rain\Scaffold */ -class_alias(\Winter\Storm\Scaffold\Console\CreateCommand::class, \October\Rain\Scaffold\Console\CreateCommand::class); -class_alias(\Winter\Storm\Scaffold\Console\CreateComponent::class, \October\Rain\Scaffold\Console\CreateComponent::class); -class_alias(\Winter\Storm\Scaffold\Console\CreateController::class, \October\Rain\Scaffold\Console\CreateController::class); -class_alias(\Winter\Storm\Scaffold\Console\CreateFormWidget::class, \October\Rain\Scaffold\Console\CreateFormWidget::class); -class_alias(\Winter\Storm\Scaffold\Console\CreateModel::class, \October\Rain\Scaffold\Console\CreateModel::class); -class_alias(\Winter\Storm\Scaffold\Console\CreatePlugin::class, \October\Rain\Scaffold\Console\CreatePlugin::class); -class_alias(\Winter\Storm\Scaffold\Console\CreateReportWidget::class, \October\Rain\Scaffold\Console\CreateReportWidget::class); -class_alias(\Winter\Storm\Scaffold\Console\CreateTheme::class, \October\Rain\Scaffold\Console\CreateTheme::class); class_alias(\Winter\Storm\Scaffold\GeneratorCommand::class, \October\Rain\Scaffold\GeneratorCommand::class); -class_alias(\Winter\Storm\Scaffold\ScaffoldServiceProvider::class, \October\Rain\Scaffold\ScaffoldServiceProvider::class); /** * Alias October\Rain\Support @@ -373,7 +353,7 @@ class_alias(\Winter\Storm\Support\Facades\Input::class, \October\Rain\Support\Fa class_alias(\Winter\Storm\Support\Facades\Mail::class, \October\Rain\Support\Facades\Mail::class); class_alias(\Winter\Storm\Support\Facades\Markdown::class, \October\Rain\Support\Facades\Markdown::class); class_alias(\Winter\Storm\Support\Facades\Schema::class, \October\Rain\Support\Facades\Schema::class); -class_alias(\Winter\Storm\Support\Facades\Str::class, \October\Rain\Support\Facades\Str::class); +class_alias(\Winter\Storm\Support\Str::class, \October\Rain\Support\Facades\Str::class); class_alias(\Winter\Storm\Support\Facades\Twig::class, \October\Rain\Support\Facades\Twig::class); class_alias(\Winter\Storm\Support\Facades\Url::class, \October\Rain\Support\Facades\Url::class); class_alias(\Winter\Storm\Support\Facades\Validator::class, \October\Rain\Support\Facades\Validator::class); diff --git a/src/Support/helpers-array.php b/src/Support/helpers-array.php index 78efc93d0..e07c42f03 100644 --- a/src/Support/helpers-array.php +++ b/src/Support/helpers-array.php @@ -152,7 +152,7 @@ function array_last($array, callable $callback = null, $default = null) * Flatten a multi-dimensional array into a single level. * * @param array $array - * @param int $depth + * @param float|int $depth * @return array */ function array_flatten($array, $depth = INF) diff --git a/src/Support/helpers.php b/src/Support/helpers.php index d96964aac..6863d796f 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -2,7 +2,6 @@ use Winter\Storm\Support\Collection; -require_once("polyfills.php"); require_once("helpers-array.php"); require_once("helpers-paths.php"); require_once("helpers-str.php"); @@ -21,7 +20,7 @@ function e($value, $doubleEncode = false) return $value->toHtml(); } - return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', $doubleEncode); + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8', $doubleEncode); } } @@ -116,7 +115,7 @@ function post($name = null, $default = null) function input($name = null, $default = null) { if ($name === null) { - return Input::all(); + return \Winter\Storm\Support\Facades\Input::all(); } /* @@ -126,21 +125,18 @@ function input($name = null, $default = null) $name = implode('.', Winter\Storm\Html\Helper::nameToArray($name)); } - return Input::get($name, $default); + return \Winter\Storm\Support\Facades\Input::get($name, $default); } } if (!function_exists('trace_log')) { /** * Writes a trace message to a log file. - * @param mixed $message Specifies a message to log. The message can be an object, array or string. - * @param string $level Specifies a level to use. If this parameter is omitted, the default listener will be used (info). + * @param Exception|array|object|string... $messages * @return void */ - function trace_log() + function trace_log(...$messages) { - $messages = func_get_args(); - foreach ($messages as $message) { $level = 'info'; @@ -159,11 +155,12 @@ function trace_log() if (!function_exists('traceLog')) { /** * Alias for trace_log() + * @param Exception|array|object|string... $messages * @return void */ - function traceLog() + function traceLog(...$messages) { - call_user_func_array('trace_log', func_get_args()); + call_user_func_array('trace_log', $messages); } } diff --git a/src/Support/polyfills.php b/src/Support/polyfills.php deleted file mode 100644 index 58f564b26..000000000 --- a/src/Support/polyfills.php +++ /dev/null @@ -1,31 +0,0 @@ -= 8.0 - * - * @param string $haystack - * @param string|array $needles - * @return bool - */ - function str_contains($haystack, $needles) - { - return Str::contains($haystack, $needles); - } -} - -if (!function_exists('is_countable')) { - /** - * Polyfill for `is_countable` method provided in PHP >= 7.3 - * - * @param mixed $var - * @return boolean - */ - function is_countable($value) - { - return (is_array($value) || $value instanceof Countable); - } -} diff --git a/src/Translation/FileLoader.php b/src/Translation/FileLoader.php index 488a863e0..86e65954d 100644 --- a/src/Translation/FileLoader.php +++ b/src/Translation/FileLoader.php @@ -5,44 +5,67 @@ class FileLoader extends FileLoaderBase { /** - * Load a namespaced translation group. + * Load a local namespaced translation group for overrides. + * + * This is an override from the base Laravel functionality that allows "xx-xx" locale format + * files as well as "xx_XX" locale format files. The "xx_XX" format is considered authorative. * + * @param array $lines * @param string $locale * @param string $group * @param string $namespace * @return array */ - protected function loadNamespaced($locale, $group, $namespace) + protected function loadNamespaceOverrides(array $lines, $locale, $group, $namespace) { - if (isset($this->hints[$namespace])) { - $lines = $this->loadPath($this->hints[$namespace], $locale, $group); + $namespace = str_replace('.', '/', $namespace); + + $file = "{$this->path}/{$locale}/{$namespace}/{$group}.php"; - if (is_array($lines)) { - return $this->loadNamespaceOverrides($lines, $locale, $group, $namespace); + if ($this->files->exists($file)) { + return array_replace_recursive($lines, $this->files->getRequire($file)); + } + + // Try "xx-xx" format + $locale = str_replace('_', '-', strtolower($locale)); + + if ("{$this->path}/{$locale}/{$namespace}/{$group}.php" !== $file) { + $file = "{$this->path}/{$locale}/{$namespace}/{$group}.php"; + + if ($this->files->exists($file)) { + return array_replace_recursive($lines, $this->files->getRequire($file)); } } - return []; + return $lines; } /** - * Load a local namespaced translation group for overrides. + * Load a locale from a given path. * - * @param array $lines + * This is an override from the base Laravel functionality that allows "xx-xx" locale format + * files as well as "xx_XX" locale format files. The "xx_XX" format is considered authorative. + * + * @param string $path * @param string $locale * @param string $group - * @param string $namespace * @return array */ - protected function loadNamespaceOverrides(array $lines, $locale, $group, $namespace) + protected function loadPath($path, $locale, $group) { - $namespace = str_replace('.', '/', $namespace); - $file = "{$this->path}/{$locale}/{$namespace}/{$group}.php"; + if ($this->files->exists($full = "{$path}/{$locale}/{$group}.php")) { + return $this->files->getRequire($full); + } - if ($this->files->exists($file)) { - return array_replace_recursive($lines, $this->files->getRequire($file)); + // Try "xx-xx" format + $locale = str_replace('_', '-', strtolower($locale)); + + if ("{$path}/{$locale}/{$group}.php" !== $full) { + if ($this->files->exists($full = "{$path}/{$locale}/{$group}.php")) { + return $this->files->getRequire($full); + } } - return $lines; + return []; } } diff --git a/src/Translation/TranslationServiceProvider.php b/src/Translation/TranslationServiceProvider.php index 05fc9bd4e..0cbfc4abf 100644 --- a/src/Translation/TranslationServiceProvider.php +++ b/src/Translation/TranslationServiceProvider.php @@ -35,7 +35,7 @@ public function register() protected function registerLoader() { $this->app->singleton('translation.loader', function ($app) { - return new FileLoader($app['files'], $app['path'].'/lang'); + return new FileLoader($app['files'], $app['path.lang']); }); } diff --git a/src/Translation/Translator.php b/src/Translation/Translator.php index ff20d530a..aef6454f1 100644 --- a/src/Translation/Translator.php +++ b/src/Translation/Translator.php @@ -11,14 +11,12 @@ */ class Translator extends TranslatorBase { - use \Winter\Storm\Support\Traits\KeyParser; - const CORE_LOCALE = 'en'; /** * The event dispatcher instance. * - * @var \Illuminate\Contracts\Events\Dispatcher|\Winter\Storm\Events\Dispatcher + * @var \Illuminate\Contracts\Events\Dispatcher|\Winter\Storm\Events\Dispatcher|null */ protected $events; @@ -51,75 +49,41 @@ public function trans($key, array $replace = [], $locale = null) */ public function get($key, array $replace = [], $locale = null, $fallback = true) { - /** - * @event translator.beforeResolve - * Fires before the translator resolves the requested language key - * - * >**NOTE:** It is highly recommended to use [project level localization overrides](https://wintercms.com/docs/plugin/localization#overriding) before reaching for this event. - * - * Example usage (overrides the value returned for a specific language key): - * - * Event::listen('translator.beforeResolve', function ((string) $key, (array) $replace, (string|null) $locale) { - * if ($key === 'my.custom.key') { - * return 'My overriding value'; - * } - * }); - * - */ - if (isset($this->events) && - ($line = $this->events->fire('translator.beforeResolve', [$key, $replace, $locale], true))) { + if ($line = $this->getValidationSpecific($key, $replace, $locale)) { return $line; } - $locale = $locale ?: $this->locale; - - // For JSON translations, there is only one file per locale, so we will simply load - // that file and then we will be ready to check the array for the key. These are - // only one level deep so we do not need to do any fancy searching through it. - $this->load('*', '*', $locale); - - $line = $this->loaded['*']['*'][$locale][$key] ?? null; - - // If we can't find a translation for the JSON key, we will attempt to translate it - // using the typical translation file. This way developers can always just use a - // helper such as __ instead of having to pick between trans or __ with views. - if (!isset($line)) { - if ($line = $this->getValidationSpecific($key, $replace, $locale)) { - return $line; - } - - list($namespace, $group, $item) = $this->parseKey($key); + return parent::get($key, $replace, $locale, $fallback); + } - if (is_null($namespace)) { - $namespace = '*'; + /** + * Set the language string value for a given key in a given locale. + * + * If no locale is provided, the language string will be set for the default locale. + */ + public function set(array|string $key, array|string|null $value = null, ?string $locale = null): void + { + if (is_array($key)) { + foreach ($key as $itemKey => $itemValue) { + $this->set($itemKey, $itemValue, $locale); } - - // Here we will get the locale that should be used for the language line. If one - // was not passed, we will use the default locales which was given to us when - // the translator was instantiated. Then, we can load the lines and return. - foreach ($this->parseLocale($locale, $fallback) as $locale) { - $line = $this->getLine( - $namespace, - $group, - $locale, - $item, - $replace - ); - - if (!is_null($line)) { - break; + } else { + $locale = $locale ?: $this->locale; + + $this->load('*', '*', $locale); + + if (is_array($value)) { + foreach ($value as $langKey => $langValue) { + if (is_array($langValue)) { + $this->set($key . '.' . $langKey, $langValue, $locale); + } else { + $this->loaded['*']['*'][$locale][$key . '.' . $langKey] = $langValue; + } } + } else { + $this->loaded['*']['*'][$locale][$key] = $value; } } - - // If the line doesn't exist, we will return back the key which was requested as - // that will be quick to spot in the UI if language keys are wrong or missing - // from the application's language files. Otherwise we can return the line. - if (!isset($line)) { - return $this->makeReplacements($key, $replace); - } - - return $line; } /** @@ -142,7 +106,7 @@ public function transChoice($key, $number, array $replace = [], $locale = null) * @param string $key * @param array $replace * @param string $locale - * @return string + * @return string|null */ protected function getValidationSpecific($key, $replace, $locale) { @@ -162,54 +126,18 @@ protected function getValidationSpecific($key, $replace, $locale) } /** - * Get a translation according to an integer value. - * - * @param string $key - * @param int|array|\Countable $number - * @param array $replace - * @param string $locale - * @return string + * @inheritDoc */ - public function choice($key, $number, array $replace = [], $locale = null) + protected function localeForChoice($locale) { - $line = $this->get( - $key, - $replace, - $locale = $this->localeForChoice($locale) - ); - - // If the given "number" is actually an array or countable we will simply count the - // number of elements in an instance. This allows developers to pass an array of - // items without having to count it on their end first which gives bad syntax. - if (is_array($number) || $number instanceof Countable) { - $number = count($number); - } + $locale = parent::localeForChoice($locale); - // Format locale for MessageSelector - if (strpos($locale, '-') !== false) { + if (str_contains($locale, '-')) { $localeParts = explode('-', $locale, 2); $locale = $localeParts[0] . '_' . strtoupper($localeParts[1]); } - $replace['count'] = $number; - - return $this->makeReplacements($this->getSelector()->choose($line, $number, $locale), $replace); - } - - /** - * Get the array of locales to be checked. - * - * @param string|null $locale - * @param bool $fallback - * @return array - */ - protected function parseLocale($locale, $fallback) - { - $locales = $fallback ? $this->localeArray($locale) : [$locale ?: $this->locale]; - - $locales[] = static::CORE_LOCALE; - - return $locales; + return $locale; } /** @@ -232,7 +160,24 @@ public function parseKey($key) } /** - * Register a namespace alias + * Get the array of locales to be checked. + * + * @param string|null $locale + * @return array + */ + protected function localeArray($locale) + { + $locales = array_values(parent::localeArray($locale)); + + if (!in_array(static::CORE_LOCALE, $locales)) { + $locales[] = static::CORE_LOCALE; + } + + return $locales; + } + + /** + * Register a namespace alias. * * @param string $namespace The namespace to register an alias for. Example: winter.blog * @param string $alias The alias to register. Example: rainlab.blog diff --git a/src/Validation/Concerns/ValidatesEmail.php b/src/Validation/Concerns/ValidatesEmail.php index 8487c9a6e..1f4c5ffc8 100644 --- a/src/Validation/Concerns/ValidatesEmail.php +++ b/src/Validation/Concerns/ValidatesEmail.php @@ -5,7 +5,7 @@ use Egulias\EmailValidator\Validation\MultipleValidationWithAnd; use Egulias\EmailValidator\Validation\NoRFCWarningsValidation; use Egulias\EmailValidator\Validation\RFCValidation; -use Egulias\EmailValidator\Validation\SpoofCheckValidation; +use Egulias\EmailValidator\Validation\Extra\SpoofCheckValidation; use Illuminate\Validation\Concerns\FilterEmailValidation; trait ValidatesEmail diff --git a/src/Validation/Validator.php b/src/Validation/Validator.php index b26b0eee9..c6d175a92 100644 --- a/src/Validation/Validator.php +++ b/src/Validation/Validator.php @@ -13,7 +13,7 @@ */ class Validator extends BaseValidator implements ValidatorContract { - use \Winter\Storm\Validation\Concerns\ValidatesEmail; + use Concerns\ValidatesEmail; use Concerns\FormatsMessages; /** diff --git a/tests/Assetic/MockAsset.php b/tests/Assetic/MockAsset.php deleted file mode 100644 index e3c56e802..000000000 --- a/tests/Assetic/MockAsset.php +++ /dev/null @@ -1,86 +0,0 @@ -content = $content; - } - - public function ensureFilter(FilterInterface $filter) - { - } - - public function getFilters() - { - } - - public function clearFilters() - { - } - - public function load(FilterInterface $additionalFilter = null) - { - } - - public function dump(FilterInterface $additionalFilter = null) - { - } - - public function getContent() - { - return $this->content; - } - - public function setContent($content) - { - $this->content = $content; - } - - public function getSourceRoot() - { - } - - public function getSourcePath() - { - } - - public function getSourceDirectory() - { - } - - public function getTargetPath() - { - } - - public function setTargetPath($targetPath) - { - } - - public function getLastModified() - { - } - - public function getVars() - { - } - - public function setValues(array $values) - { - } - - public function getValues() - { - } -} diff --git a/tests/Assetic/StylesheetMinifyTest.php b/tests/Assetic/StylesheetMinifyTest.php deleted file mode 100644 index a5bd16eaa..000000000 --- a/tests/Assetic/StylesheetMinifyTest.php +++ /dev/null @@ -1,120 +0,0 @@ -filterDump($mockAsset); - - $this->assertEquals($output, $mockAsset->getContent()); - } - - public function testEmptyClassPreserve() - { - $input = <<filterDump($mockAsset); - - $this->assertEquals($output, $mockAsset->getContent()); - } - - public function testSpecialCommentPreservation() - { - $input = 'body {/*! Keep me */}'; - $output = 'body{/*! Keep me */}'; - - $mockAsset = new MockAsset($input); - $result = new StylesheetMinify(); - $result->filterDump($mockAsset); - - $this->assertEquals($output, $mockAsset->getContent()); - } - - public function testCommentRemoval() - { - $input = 'body{/* First comment */} /* Second comment */'; - $output = 'body{}'; - - $mockAsset = new MockAsset($input); - $result = new StylesheetMinify(); - $result->filterDump($mockAsset); - - $this->assertEquals($output, $mockAsset->getContent()); - } - - public function testCommentPreservationInVar() - { - $input = '--ring-inset: var(--empty, /*!*/ /*!*/);'; - $output = '--ring-inset:var(--empty,/*!*/ /*!*/);'; - - $mockAsset = new MockAsset($input); - $result = new StylesheetMinify(); - $result->filterDump($mockAsset); - - $this->assertEquals($output, $mockAsset->getContent()); - } - - public function testUnitPreservationInVar() - { - $input = '--offset-width: 0px'; - $output = '--offset-width:0'; - - $mockAsset = new MockAsset($input); - $result = new StylesheetMinify(); - $result->filterDump($mockAsset); - - $this->assertEquals($output, $mockAsset->getContent()); - } - - public function testAttributeSelectorsWithLess() - { - $input = <<filterDump($mockAsset); - - $this->assertEquals($output, $mockAsset->getContent()); - } -} diff --git a/tests/Database/Fixtures/CustomMorphPivot.php b/tests/Database/Fixtures/CustomMorphPivot.php new file mode 100644 index 000000000..a3ff91ca9 --- /dev/null +++ b/tests/Database/Fixtures/CustomMorphPivot.php @@ -0,0 +1,9 @@ +assertNull($model->name); } + public function testVisibleAttributes() + { + $model = TestModelVisible::create([ + 'name' => 'Visible Test', + 'data' => 'Test data', + 'description' => 'Test description', + 'meta' => 'Some meta data' + ]); + + $this->assertArrayNotHasKey('meta', $model->toArray()); + + $model->addVisible('meta'); + + $this->assertArrayHasKey('meta', $model->toArray()); + } + + public function testHiddenAttributes() + { + $model = TestModelHidden::create([ + 'name' => 'Hidden Test', + 'data' => 'Test data', + 'description' => 'Test description', + 'meta' => 'Some meta data' + ]); + + $this->assertArrayHasKey('description', $model->toArray()); + + $model->addHidden('description'); + + $this->assertArrayNotHasKey('description', $model->toArray()); + } + protected function createTable() { - $this->db->schema()->create('test_model', function ($table) { + $this->getBuilder()->create('test_model', function ($table) { $table->increments('id'); $table->string('name')->nullable(); $table->text('data')->nullable(); + $table->text('description')->nullable(); + $table->text('meta')->nullable(); $table->boolean('on_guard')->nullable(); $table->timestamps(); }); @@ -131,3 +165,37 @@ public function beforeSave() } } } + +class TestModelVisible extends Model +{ + public $fillable = [ + 'name', + 'data', + 'description', + 'meta' + ]; + + public $visible = [ + 'id', + 'name', + 'description' + ]; + + public $table = 'test_model'; +} + +class TestModelHidden extends Model +{ + public $fillable = [ + 'name', + 'data', + 'description', + 'meta' + ]; + + public $hidden = [ + 'meta', + ]; + + public $table = 'test_model'; +} diff --git a/tests/Database/MorphPivotTest.php b/tests/Database/MorphPivotTest.php new file mode 100644 index 000000000..2bd933775 --- /dev/null +++ b/tests/Database/MorphPivotTest.php @@ -0,0 +1,144 @@ +createTables(); + } + + public function testCreateMorphyToManyRelationAndCheckForMorphPivot() + { + // Create a couple of tags + $cool = Tag::create([ + 'name' => 'Cool', + ]); + $awesome = Tag::create([ + 'name' => 'Awesome', + ]); + + // Create a post + $post = Post::create([ + 'title' => 'Check this out', + 'body' => 'It is pretty cool and pretty awesome too', + ]); + + // Attach tags + $post->tags()->attach($cool); + $post->tags()->attach($awesome); + + // Get first tag and get a pivot instance + $pivot = $post->tags()->first()->pivot; + + $this->assertInstanceOf(MorphPivot::class, $pivot); + $this->assertEquals('0', $pivot->hidden); + } + + public function testCreateMorphyToManyRelationAndCheckForCustomMorphPivot() + { + // Create a couple of tags + $cool = Tag::create([ + 'name' => 'Cool', + ]); + $awesome = Tag::create([ + 'name' => 'Awesome', + ]); + + // Create a post + $post = CustomPost::create([ + 'title' => 'Check this out', + 'body' => 'It is pretty cool and pretty awesome too', + ]); + + // Attach tags + $post->tags()->attach($cool); + $post->tags()->attach($awesome); + + // Get first tag and get a pivot instance + $pivot = $post->tags()->first()->pivot; + + $this->assertInstanceOf(CustomMorphPivot::class, $pivot); + } + + protected function createTables() + { + $this->getBuilder()->create('posts', function ($table) { + $table->increments('id'); + $table->string('title'); + $table->text('body')->nullable(); + $table->timestamps(); + }); + + $this->getBuilder()->create('tags', function ($table) { + $table->increments('id'); + $table->string('name'); + $table->timestamps(); + }); + + $this->getBuilder()->create('taggings', function ($table) { + $table->increments('id'); + $table->integer('tag_id')->unsigned(); + $table->morphs('taggable'); + $table->boolean('hidden')->default(0); + $table->timestamps(); + }); + } +} + +class Post extends Model +{ + public $table = 'posts'; + + public $fillable = [ + 'title', + 'body', + ]; + + public $morphToMany = [ + 'tags' => [ + Tag::class, + 'table' => 'taggings', + 'name' => 'taggable', + 'pivot' => ['hidden'], + ], + ]; +} + +class CustomPost extends Post +{ + public $morphToMany = [ + 'tags' => [ + Tag::class, + 'table' => 'taggings', + 'name' => 'taggable', + 'pivot' => ['hidden'], + 'pivotModel' => CustomMorphPivot::class, + ], + ]; +} + +class Tagging extends Model +{ + public $table = 'taggings'; + + protected $casts = [ + 'hidden' => 'boolean', + ]; +} + +class Tag extends Model +{ + public $table = 'tags'; + + public $fillable = [ + 'name', + ]; +} diff --git a/tests/Database/QueryBuilderTest.php b/tests/Database/QueryBuilderTest.php index 67554578e..0d2387ce9 100644 --- a/tests/Database/QueryBuilderTest.php +++ b/tests/Database/QueryBuilderTest.php @@ -94,96 +94,10 @@ public function testSelectConcat() ); } - public function testUpsert() - { - // MySQL - $builder = $this->getMySqlBuilder(); - $builder->getConnection() - ->expects($this->once()) - ->method('affectingStatement') - ->with('insert into `users` (`email`, `name`) values (?, ?), (?, ?) on duplicate key update `email` = values(`email`), `name` = values(`name`)', ['foo', 'bar', 'foo2', 'bar2']) - ->willReturn(2); - $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email'); - $this->assertEquals(2, $result); - - // PostgreSQL - $builder = $this->getPostgresBuilder(); - $builder->getConnection() - ->expects($this->once()) - ->method('affectingStatement') - ->with('insert into "users" ("email", "name") values (?, ?), (?, ?) on conflict ("email") do update set "email" = "excluded"."email", "name" = "excluded"."name"', ['foo', 'bar', 'foo2', 'bar2']) - ->willReturn(2); - $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email'); - $this->assertEquals(2, $result); - - // SQLite - $builder = $this->getSQLiteBuilder(); - $builder->getConnection() - ->expects($this->once()) - ->method('affectingStatement') - ->with('insert into "users" ("email", "name") values (?, ?), (?, ?) on conflict ("email") do update set "email" = "excluded"."email", "name" = "excluded"."name"', ['foo', 'bar', 'foo2', 'bar2']) - ->willReturn(2); - $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email'); - $this->assertEquals(2, $result); - - // SQL Server - $builder = $this->getSqlServerBuilder(); - $builder->getConnection() - ->expects($this->once()) - ->method('affectingStatement') - ->with('merge [users] using (values (?, ?), (?, ?)) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [email] = [laravel_source].[email], [name] = [laravel_source].[name] when not matched then insert ([email], [name]) values ([email], [name])', ['foo', 'bar', 'foo2', 'bar2']) - ->willReturn(2); - $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email'); - $this->assertEquals(2, $result); - } - - public function testUpsertWithUpdateColumns() - { - // MySQL - $builder = $this->getMySqlBuilder(); - $builder->getConnection() - ->expects($this->once()) - ->method('affectingStatement') - ->with('insert into `users` (`email`, `name`) values (?, ?), (?, ?) on duplicate key update `name` = values(`name`)', ['foo', 'bar', 'foo2', 'bar2']) - ->willReturn(2); - $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']); - $this->assertEquals(2, $result); - - // PostgreSQL - $builder = $this->getPostgresBuilder(); - $builder->getConnection() - ->expects($this->once()) - ->method('affectingStatement') - ->with('insert into "users" ("email", "name") values (?, ?), (?, ?) on conflict ("email") do update set "name" = "excluded"."name"', ['foo', 'bar', 'foo2', 'bar2']) - ->willReturn(2); - $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']); - $this->assertEquals(2, $result); - - // SQLite - $builder = $this->getSQLiteBuilder(); - $builder->getConnection() - ->expects($this->once()) - ->method('affectingStatement') - ->with('insert into "users" ("email", "name") values (?, ?), (?, ?) on conflict ("email") do update set "name" = "excluded"."name"', ['foo', 'bar', 'foo2', 'bar2']) - ->willReturn(2); - $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']); - $this->assertEquals(2, $result); - - // SQL Server - $builder = $this->getSqlServerBuilder(); - $builder->getConnection() - ->expects($this->once()) - ->method('affectingStatement') - ->with('merge [users] using (values (?, ?), (?, ?)) [laravel_source] ([email], [name]) on [laravel_source].[email] = [users].[email] when matched then update set [name] = [laravel_source].[name] when not matched then insert ([email], [name]) values ([email], [name])', ['foo', 'bar', 'foo2', 'bar2']) - ->willReturn(2); - $result = $builder->from('users')->upsert([['email' => 'foo', 'name' => 'bar'], ['name' => 'bar2', 'email' => 'foo2']], 'email', ['name']); - $this->assertEquals(2, $result); - } - - protected function getConnection($connection = null) + protected function getConnection($connection = null, $table = null) { if ($connection) { - return parent::getConnection($connection); + return parent::getConnection($connection, $table); } $connection = $this->getMockBuilder(ConnectionInterface::class) @@ -210,13 +124,13 @@ protected function getConnection($connection = null) 'rollBack', 'transactionLevel', 'pretend', - ]) - ->addMethods([ 'getDatabaseName', + 'getConfig', ]) ->getMock(); $connection->method('getDatabaseName')->willReturn('database'); + $connection->method('getConfig')->with('use_upsert_alias')->willReturn(false); return $connection; } diff --git a/tests/Database/RelationsTest.php b/tests/Database/RelationsTest.php index 93a1a854e..88d2c55df 100644 --- a/tests/Database/RelationsTest.php +++ b/tests/Database/RelationsTest.php @@ -189,7 +189,7 @@ public function testDefinedMorphsRelation() protected function createTables() { - $this->db->schema()->create('posts', function ($table) { + $this->getBuilder()->create('posts', function ($table) { $table->increments('id'); $table->string('title')->default(''); $table->boolean('published')->nullable(); @@ -197,14 +197,14 @@ protected function createTables() $table->timestamps(); }); - $this->db->schema()->create('terms', function ($table) { + $this->getBuilder()->create('terms', function ($table) { $table->increments('id'); $table->string('type')->index(); $table->string('name'); $table->timestamps(); }); - $this->db->schema()->create('posts_terms', function ($table) { + $this->getBuilder()->create('posts_terms', function ($table) { $table->primary(['post_id', 'term_id']); $table->unsignedInteger('post_id'); $table->unsignedInteger('term_id'); @@ -212,13 +212,13 @@ protected function createTables() $table->timestamps(); }); - $this->db->schema()->create('categories', function ($table) { + $this->getBuilder()->create('categories', function ($table) { $table->increments('id'); $table->string('name'); $table->timestamps(); }); - $this->db->schema()->create('posts_categories', function ($table) { + $this->getBuilder()->create('posts_categories', function ($table) { $table->primary(['post_id', 'category_id']); $table->unsignedInteger('post_id'); $table->unsignedInteger('category_id'); @@ -262,7 +262,7 @@ protected function seedTables() } } -class Category extends \October\Rain\Database\Model +class Category extends \Winter\Storm\Database\Model { public $table = 'categories'; diff --git a/tests/Database/SortableTest.php b/tests/Database/SortableTest.php index fc6f0d75d..8b16990fc 100644 --- a/tests/Database/SortableTest.php +++ b/tests/Database/SortableTest.php @@ -10,6 +10,14 @@ public function testOrderByIsAutomaticallyAdded() $this->assertEquals('select * from "test" order by "sort_order" asc', $query); } + public function testCustomSortOrderByIsAutomaticallyAdded() + { + $model = new TestCustomSortableModel(); + $query = $model->newQuery()->toSql(); + + $this->assertEquals('select * from "test" order by "rank" asc', $query); + } + public function testOrderByCanBeOverridden() { $model = new TestSortableModel(); @@ -18,6 +26,13 @@ public function testOrderByCanBeOverridden() $this->assertEquals('select * from "test" order by "name" asc, "email" desc', $query1); $this->assertEquals('select * from "test" order by "sort_order" asc, "name" asc', $query2); + + $model = new TestCustomSortableModel(); + $query1 = $model->newQuery()->orderBy('name')->orderBy('email', 'desc')->toSql(); + $query2 = $model->newQuery()->orderBy('sort_order')->orderBy('name')->toSql(); + + $this->assertEquals('select * from "test" order by "name" asc, "email" desc', $query1); + $this->assertEquals('select * from "test" order by "sort_order" asc, "name" asc', $query2); } } @@ -27,3 +42,12 @@ class TestSortableModel extends \Winter\Storm\Database\Model protected $table = 'test'; } + +class TestCustomSortableModel extends \Winter\Storm\Database\Model +{ + use \Winter\Storm\Database\Traits\Sortable; + + const SORT_ORDER = 'rank'; + + protected $table = 'test'; +} diff --git a/tests/Database/Traits/ArraySourceTest.php b/tests/Database/Traits/ArraySourceTest.php new file mode 100644 index 000000000..52b9c9628 --- /dev/null +++ b/tests/Database/Traits/ArraySourceTest.php @@ -0,0 +1,250 @@ +tmpDbPath = dirname(dirname(__DIR__)) . '/tmp'; + $this->file = new Filesystem(); + + // Create temp directory for SQLite DBs + $this->file->deleteDirectory($this->tmpDbPath); + $this->file->makeDirectory($this->tmpDbPath, 0777, true, true); + } + + public function tearDown(): void + { + $this->file->deleteDirectory($this->tmpDbPath); + + parent::tearDown(); + } + + public function testAll(): void + { + $records = ArrayModel::get(); + + $this->assertEquals(4, $records->count()); + $this->assertEquals('Ben Thomson', $records->first()->name); + $this->assertEquals(2019, $records->first()->start_year); + $this->assertEquals('Maintainer', $records->last()->role); + $this->assertEquals(2021, $records->last()->start_year); + } + + public function testGet(): void + { + $record = ArrayModel::find(2); + + $this->assertEquals('Luke Towers', $record->name); + $this->assertEquals('Lead Maintainer', $record->role); + } + + public function testWhere(): void + { + $records = ArrayModel::where('role', 'Maintainer'); + + $this->assertEquals(3, $records->count()); + $this->assertEquals([ + 'Ben Thomson', + 'Marc Jauvin', + 'Jack Wilkinson', + ], $records->pluck('name')->toArray()); + } + + public function testOrder(): void + { + $records = ArrayModel::orderBy('name'); + + $this->assertEquals(4, $records->count()); + $this->assertEquals([ + 'Ben Thomson', + 'Jack Wilkinson', + 'Luke Towers', + 'Marc Jauvin', + ], $records->pluck('name')->toArray()); + } + + public function testLimit(): void + { + $records = ArrayModel::limit(2)->get(); + + $this->assertEquals(2, $records->count()); + $this->assertEquals([ + 'Ben Thomson', + 'Luke Towers', + ], $records->pluck('name')->toArray()); + } + + public function testRelations(): void + { + $records = Country::get(); + + $this->assertEquals(2, $records->count()); + $this->assertEquals(8, $records->first()->states()->count()); // Australia + $this->assertEquals(10, $records->last()->states()->count()); // Canada + + $this->assertEquals(1, $records->first()->states()->first()->id); + $this->assertEquals('Western Australia', $records->first()->states()->first()->name); + + $this->assertEquals(18, $records->last()->states()->get()->last()->id); + $this->assertEquals('Newfoundland and Labrador', $records->last()->states()->get()->last()->name); + } +} + +class ArrayModel extends \Winter\Storm\Database\Model +{ + use \Winter\Storm\Database\Traits\ArraySource; + + public $records = [ + [ + 'id' => 1, + 'name' => 'Ben Thomson', + 'role' => 'Maintainer', + 'start_year' => '2019', + ], + [ + 'id' => 2, + 'name' => 'Luke Towers', + 'role' => 'Lead Maintainer', + 'start_year' => '2016', + ], + [ + 'id' => 3, + 'name' => 'Marc Jauvin', + 'role' => 'Maintainer', + 'start_year' => '2019', + ], + [ + 'id' => 4, + 'name' => 'Jack Wilkinson', + 'role' => 'Maintainer', + 'start_year' => '2021', + ], + ]; + + public $arraySchema = [ + 'start_year' => 'integer', + ]; + + protected function arraySourceGetDbDir(): string|false + { + return dirname(dirname(__DIR__)) . '/tmp'; + } +} + +class Country extends \Winter\Storm\Database\Model +{ + use \Winter\Storm\Database\Traits\ArraySource; + + public $records = [ + [ + 'id' => 1, + 'name' => 'Australia', + ], + [ + 'id' => 2, + 'name' => 'Canada', + ], + ]; + + public $hasMany = [ + 'states' => State::class, + ]; + + protected function arraySourceGetDbDir(): string|false + { + return dirname(dirname(__DIR__)) . '/tmp'; + } +} + +class State extends \Winter\Storm\Database\Model +{ + use \Winter\Storm\Database\Traits\ArraySource; + + public $records = [ + [ + 'country_id' => 1, + 'name' => 'Western Australia', + ], + [ + 'country_id' => 1, + 'name' => 'South Australia', + ], + [ + 'country_id' => 1, + 'name' => 'Victoria', + ], + [ + 'country_id' => 1, + 'name' => 'Australian Capital Territory', + ], + [ + 'country_id' => 1, + 'name' => 'New South Wales', + ], + [ + 'country_id' => 1, + 'name' => 'Queensland', + ], + [ + 'country_id' => 1, + 'name' => 'Northern Territory', + ], + [ + 'country_id' => 1, + 'name' => 'Tasmania', + ], + [ + 'country_id' => 2, + 'name' => 'Ontario', + ], + [ + 'country_id' => 2, + 'name' => 'Quebec', + ], + [ + 'country_id' => 2, + 'name' => 'Nova Scotia', + ], + [ + 'country_id' => 2, + 'name' => 'New Brunswick', + ], + [ + 'country_id' => 2, + 'name' => 'Manitoba', + ], + [ + 'country_id' => 2, + 'name' => 'British Columbia', + ], + [ + 'country_id' => 2, + 'name' => 'Prince Edward Island', + ], + [ + 'country_id' => 2, + 'name' => 'Saskatchewan', + ], + [ + 'country_id' => 2, + 'name' => 'Alberta', + ], + [ + 'country_id' => 2, + 'name' => 'Newfoundland and Labrador', + ], + ]; + + public $belongsTo = [ + 'country' => Country::class, + ]; + + protected function arraySourceGetDbDir(): string|false + { + return dirname(dirname(__DIR__)) . '/tmp'; + } +} diff --git a/tests/Database/Traits/EncryptableTest.php b/tests/Database/Traits/EncryptableTest.php index c68c849ae..5d2de2588 100644 --- a/tests/Database/Traits/EncryptableTest.php +++ b/tests/Database/Traits/EncryptableTest.php @@ -28,20 +28,20 @@ public function testEncryptableTrait() $this->assertEquals('test', $testModel->secret); $this->assertNotEquals('test', $testModel->attributes['secret']); $payloadOne = json_decode(base64_decode($testModel->attributes['secret']), true); - $this->assertEquals(['iv', 'value', 'mac'], array_keys($payloadOne)); + $this->assertEquals(['iv', 'value', 'mac', 'tag'], array_keys($payloadOne)); $testModel->secret = ''; $this->assertEquals('', $testModel->secret); $this->assertNotEquals('', $testModel->attributes['secret']); $payloadTwo = json_decode(base64_decode($testModel->attributes['secret']), true); - $this->assertEquals(['iv', 'value', 'mac'], array_keys($payloadTwo)); + $this->assertEquals(['iv', 'value', 'mac', 'tag'], array_keys($payloadTwo)); $this->assertNotEquals($payloadOne['value'], $payloadTwo['value']); $testModel->secret = 0; $this->assertEquals(0, $testModel->secret); $this->assertNotEquals(0, $testModel->attributes['secret']); $payloadThree = json_decode(base64_decode($testModel->attributes['secret']), true); - $this->assertEquals(['iv', 'value', 'mac'], array_keys($payloadThree)); + $this->assertEquals(['iv', 'value', 'mac', 'tag'], array_keys($payloadThree)); $this->assertNotEquals($payloadTwo['value'], $payloadThree['value']); $testModel->secret = null; @@ -51,7 +51,7 @@ public function testEncryptableTrait() protected function createTable() { - $this->db->schema()->create('secrets', function ($table) { + $this->getBuilder()->create('secrets', function ($table) { $table->increments('id'); $table->string('secret'); $table->timestamps(); diff --git a/tests/Database/Traits/PurgeableTraitTest.php b/tests/Database/Traits/PurgeableTraitTest.php index 11b234e03..3663423d0 100644 --- a/tests/Database/Traits/PurgeableTraitTest.php +++ b/tests/Database/Traits/PurgeableTraitTest.php @@ -35,7 +35,7 @@ public function testPurgeable() protected function createTables() { - $this->db->schema()->create('test_purge', function ($table) { + $this->getBuilder()->create('test_purge', function ($table) { $table->increments('id'); $table->string('name'); $table->string('data')->nullable(); diff --git a/tests/Database/Traits/SluggableTest.php b/tests/Database/Traits/SluggableTest.php index e4c7b18a5..5ed0b67c8 100644 --- a/tests/Database/Traits/SluggableTest.php +++ b/tests/Database/Traits/SluggableTest.php @@ -140,7 +140,7 @@ public function testSlugGenerationWithHardDelete() protected function createTables() { - $this->db->schema()->create('testSoftDelete', function ($table) { + $this->getBuilder()->create('testSoftDelete', function ($table) { $table->increments('id'); $table->string('name'); $table->string('slug')->unique(); @@ -148,7 +148,7 @@ protected function createTables() $table->timestamps(); }); - $this->db->schema()->create('testSoftDeleteNoUnique', function ($table) { + $this->getBuilder()->create('testSoftDeleteNoUnique', function ($table) { $table->increments('id'); $table->string('name'); $table->string('slug'); @@ -156,7 +156,7 @@ protected function createTables() $table->timestamps(); }); - $this->db->schema()->create('testSoftDeleteAllow', function ($table) { + $this->getBuilder()->create('testSoftDeleteAllow', function ($table) { $table->increments('id'); $table->string('name'); $table->string('slug')->unique(); @@ -164,7 +164,7 @@ protected function createTables() $table->timestamps(); }); - $this->db->schema()->create('test', function ($table) { + $this->getBuilder()->create('test', function ($table) { $table->increments('id'); $table->string('name'); $table->string('slug')->unique(); diff --git a/tests/Database/Traits/ValidationTest.php b/tests/Database/Traits/ValidationTest.php index b0161203a..76ddc7cfd 100644 --- a/tests/Database/Traits/ValidationTest.php +++ b/tests/Database/Traits/ValidationTest.php @@ -1,5 +1,8 @@ db = new CapsuleManager; - $this->db->addConnection([ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => '' - ]); + parent::setUp(); - $this->db->setAsGlobal(); - $this->db->bootEloquent(); + $config = [ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]; + App::make(ConnectionFactory::class)->make($config, 'testing'); + DB::setDefaultConnection('testing'); Model::setEventDispatcher(new Dispatcher()); } @@ -25,8 +26,18 @@ public function setUp(): void public function tearDown(): void { $this->flushModelEventListeners(); + parent::tearDown(); - unset($this->db); + } + + /** + * Returns an instance of the schema builder for the test database. + * + * @return \Illuminate\Database\Schema\Builder + */ + protected function getBuilder() + { + return DB::connection()->getSchemaBuilder(); } /** diff --git a/tests/Events/DispatcherTest.php b/tests/Events/DispatcherTest.php new file mode 100644 index 000000000..2cf08f419 --- /dev/null +++ b/tests/Events/DispatcherTest.php @@ -0,0 +1,160 @@ +listen('test.test', function () use (&$magic_value) { + $magic_value = true; + }); + $dispatcher->fire('test.test'); + $this->assertTrue($magic_value); + } + + /** + * Test closure usage + */ + public function testTypedClosureListen() + { + $magic_value = false; + $dispatcher = new Dispatcher(); + $dispatcher->listen(function (EventTest $event) use (&$magic_value) { + $magic_value = true; + }); + $dispatcher->dispatch('test.test'); + $this->assertFalse($magic_value); + $dispatcher->dispatch(new EventTest); + $this->assertTrue($magic_value); + } + + public function testClosureWithValueArgument() + { + $original = false; + + $dispatcher = new Dispatcher(); + $dispatcher->listen('test', function ($value) { + $value = true; + }); + $dispatcher->dispatch('test', [$original]); + + $this->assertFalse($original); + } + + public function testClosureWithReferenceArgument() + { + $original = false; + + $dispatcher = new Dispatcher(); + $dispatcher->listen('test', function (&$value) { + $value = true; + }); + $dispatcher->dispatch('test', [&$original]); + + $this->assertTrue($original); + } + + public function testStringEventPriorities() + { + $magic_value = 0; + $dispatcher = new Dispatcher(); + + $dispatcher->listen("test.test", function () use (&$magic_value) { + $magic_value = 42; + }, 1); + $dispatcher->listen("test.test", function () use (&$magic_value) { + $magic_value = 1; + }, 2); + + $dispatcher->dispatch("test.test"); + $this->assertEquals(42, $magic_value); + } + + public function testClosurePriorities() + { + $magic_value = 0; + $dispatcher = new Dispatcher(); + + $dispatcher->listen(function (EventTest $test) use (&$magic_value) { + $magic_value = 42; + }, 1); + $dispatcher->listen(function (EventTest $test) use (&$magic_value) { + $magic_value = 1; + }, 2); + + $dispatcher->dispatch(new EventTest()); + $this->assertEquals(42, $magic_value); + } + + public function testQueuedClosurePriorities() + { + $mock_queued_closure_should_match = $this->createMock(QueuedClosure::class); + $mock_queued_closure_should_match->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = 42; + }; + $mock_queued_closure_should_match->method('resolve')->willReturn($mock_queued_closure_should_match->closure); + + $mock_queued_closure_should_not_match = $this->createMock(QueuedClosure::class); + $mock_queued_closure_should_not_match->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = 2; + }; + $mock_queued_closure_should_not_match->method('resolve')->willReturn($mock_queued_closure_should_not_match->closure); + $dispatcher = new Dispatcher(); + $magic_value = 0; + + // Test natural sorting without priority to the queued tasks to be queued. + $dispatcher->listen($mock_queued_closure_should_not_match); + $dispatcher->listen($mock_queued_closure_should_match); + $dispatcher->dispatch(new EventTest()); + $this->assertEquals(42, $magic_value); + + // Test priority sorting for the queued tasks to be queued + $magic_value = 0; + $dispatcher->listen($mock_queued_closure_should_match, 1); + $dispatcher->listen($mock_queued_closure_should_not_match, 2); + $dispatcher->dispatch(new EventTest()); + $this->assertEquals(42, $magic_value); + } + + /** + * Test whether the dispatcher accepts a QueuedClosure + */ + public function testQueuedClosureListen() + { + $magic_value = false; + $mock_queued_closure = $this->createMock(QueuedClosure::class); + $mock_queued_closure->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = true; + }; + $mock_queued_closure->method('resolve')->willReturn($mock_queued_closure->closure); + $dispatcher = new Dispatcher(); + $dispatcher->listen($mock_queued_closure); + $dispatcher->dispatch(new EventTest()); + $this->assertTrue($magic_value); + } +} diff --git a/tests/Extension/ExtendableTest.php b/tests/Extension/ExtendableTest.php index 9a283cf28..4e7a6efb7 100644 --- a/tests/Extension/ExtendableTest.php +++ b/tests/Extension/ExtendableTest.php @@ -247,6 +247,35 @@ public function testGetClassMethods() $this->assertContains('getFooAnotherWay', $methods); $this->assertNotContains('missingFunction', $methods); } + + public function testClosureSerialization() + { + $test_string = 'hello world'; + BasicExtendable::extend(function (BasicExtendable $class) use ($test_string) { + $class->addDynamicMethod('foobar', function () use ($test_string) { + $x = function () use ($test_string) { + return $test_string; + }; + return $x(); + }); + $class->addDynamicMethod('bazbal', function () use ($test_string) { + return function () use ($test_string) { + return $test_string; + }; + }); + }); + + $subject = new BasicExtendable(); + + $serialized = serialize($subject); + + $unserialized = unserialize($serialized); + + $this->assertEquals($test_string, $unserialized->foobar()); + $test = $unserialized->bazbal(); + $this->assertInstanceOf(Closure::class, $test); + $this->assertEquals($test(), $test_string); + } } // @@ -355,6 +384,10 @@ public static function getName() } } +class BasicExtendable extends Extendable +{ +} + /* * Example class with soft implement failure */ diff --git a/tests/FilesystemAdapterTest.php b/tests/FilesystemAdapterTest.php index df493820d..8dbe8ec8a 100644 --- a/tests/FilesystemAdapterTest.php +++ b/tests/FilesystemAdapterTest.php @@ -1,17 +1,17 @@ expectException(RuntimeException::class); - (new FilesystemAdapter($flysystem))->temporaryUrl('test.jpg', \Carbon\Carbon::now()->addMinutes(5)); + $adapter = new LocalFilesystemAdapter('/tmp/app'); + (new FilesystemAdapter(new Flysystem($adapter), $adapter)) + ->temporaryUrl('test.jpg', \Carbon\Carbon::now()->addMinutes(5)); } } diff --git a/tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php b/tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php index 44a1a74c0..3c6b76e40 100644 --- a/tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php +++ b/tests/Foundation/Http/Middleware/CheckForTrustedProxiesTest.php @@ -34,7 +34,7 @@ public function testUntrusted() public function testTrustedProxy() { $request = $this->createProxiedRequest(); - $request->setTrustedProxies(['173.174.200.38'], Request::HEADER_X_FORWARDED_ALL); + $request->setTrustedProxies(['173.174.200.38'], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); $this->assertEquals('192.168.10.10', $request->getClientIp()); $this->assertEquals('https', $request->getScheme()); @@ -49,7 +49,7 @@ public function testTrustedProxy() */ public function testTrustedProxyMiddlewareWithWildcard() { - $middleware = $this->createTrustedProxyMock('*', Request::HEADER_X_FORWARDED_ALL); + $middleware = $this->createTrustedProxyMock('*', 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { @@ -67,7 +67,7 @@ public function testTrustedProxyMiddlewareWithWildcard() */ public function testTrustedProxyMiddlewareWithStringIp() { - $middleware = $this->createTrustedProxyMock('173.174.200.38', Request::HEADER_X_FORWARDED_ALL); + $middleware = $this->createTrustedProxyMock('173.174.200.38', 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { @@ -85,7 +85,7 @@ public function testTrustedProxyMiddlewareWithStringIp() */ public function testTrustedProxyMiddlewareWithStringCsv() { - $middleware = $this->createTrustedProxyMock('173.174.200.38, 173.174.200.38', Request::HEADER_X_FORWARDED_ALL); + $middleware = $this->createTrustedProxyMock('173.174.200.38, 173.174.200.38', 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { @@ -103,7 +103,7 @@ public function testTrustedProxyMiddlewareWithStringCsv() */ public function testTrustedProxyMiddlewareWithArray() { - $middleware = $this->createTrustedProxyMock(['173.174.200.38', '173.174.200.38'], Request::HEADER_X_FORWARDED_ALL); + $middleware = $this->createTrustedProxyMock(['173.174.200.38', '173.174.200.38'], 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { @@ -121,7 +121,7 @@ public function testTrustedProxyMiddlewareWithArray() */ public function testUntrustedProxyMiddlewareWithArray() { - $middleware = $this->createTrustedProxyMock(['173.174.100.1', '173.174.100.2'], Request::HEADER_X_FORWARDED_ALL); + $middleware = $this->createTrustedProxyMock(['173.174.100.1', '173.174.100.2'], 'HEADER_X_FORWARDED_ALL'); $request = $this->createProxiedRequest(); $middleware->handle($request, function ($request) { @@ -255,7 +255,7 @@ protected function createProxiedRequest(array $overrides = []) ); // Reset trusted proxies and headers - $request->setTrustedProxies([], Request::HEADER_X_FORWARDED_ALL); + $request->setTrustedProxies([], Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO); return $request; } diff --git a/tests/Foundation/ProviderRepositoryTest.php b/tests/Foundation/ProviderRepositoryTest.php new file mode 100644 index 000000000..2320adc9e --- /dev/null +++ b/tests/Foundation/ProviderRepositoryTest.php @@ -0,0 +1,96 @@ +testAppDir = dirname(__DIR__) . '/tmp/test-app'; + if (!is_dir($this->testAppDir . '/storage/framework')) { + mkdir($this->testAppDir . '/storage/framework', 0777, true); + } + + $this->basePath = $this->testAppDir; + $this->app = new Application($this->basePath); + $this->app->detectEnvironment(function () { + return 'test'; + }); + } + + public function tearDown(): void + { + // Remove created files and folders + if (is_file($this->testAppDir . '/storage/framework/packages.php')) { + unlink($this->testAppDir . '/storage/framework/packages.php'); + } + if (is_dir($this->testAppDir . '/storage/framework')) { + rmdir($this->testAppDir . '/storage/framework'); + } + if (is_dir($this->testAppDir . '/storage')) { + rmdir($this->testAppDir . '/storage'); + } + if (is_dir($this->testAppDir)) { + rmdir($this->testAppDir); + } + + parent::tearDown(); + } + + public function testOriginalFunctionaliy(): void + { + $this->expectException(\Illuminate\Contracts\Container\BindingResolutionException::class); + $this->expectExceptionMessage('Target class [cache] does not exist.'); + + $files = new Filesystem; + + // Simulate loading provider + $repository = new LaravelProviderRepository($this->app, $files, $this->app->getCachedPackagesPath()); + $repository->load([ + ConfigServiceProvider::class, + CacheServiceProvider::class, + TestFixtureProvider::class, + ]); + + $this->assertEquals('Tested!', $this->app['test']); + } + + public function testWinterFunctionaliy(): void + { + $files = new Filesystem; + + // Simulate loading provider + $repository = new WinterProviderRepository($this->app, $files, $this->app->getCachedPackagesPath()); + $repository->load([ + ConfigServiceProvider::class, + CacheServiceProvider::class, + TestFixtureProvider::class, + ]); + + $this->assertEquals('Tested!', $this->app['test']); + } +} + +// Provider fixture for testing +class TestFixtureProvider extends ServiceProvider +{ + public function register() + { + // Test cache provider request - this should fail in the base functionality, but work in + // Winter's implementation + $thisValue = $this->app['cache']->get('some_value'); + + $this->app->singleton('test', function () { + return 'Tested!'; + }); + } +} diff --git a/tests/Halcyon/HalcyonModelTest.php b/tests/Halcyon/HalcyonModelTest.php index 2c810aea9..91bf97735 100644 --- a/tests/Halcyon/HalcyonModelTest.php +++ b/tests/Halcyon/HalcyonModelTest.php @@ -1,9 +1,11 @@ setDatasourceResolver(); + // Fake a request so flash messages are not sent + Input::swap(new Request()); + $this->setValidatorOnModel(); } @@ -194,9 +199,11 @@ public function testUpdatePageRenameFile() public function testUpdatePageRenameFileCase() { - $fileHelper = new Filesystem; + $originalFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/Test.htm'; + $renamedFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/test.htm'; - @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/Test.htm'); + @unlink($originalFile); + @unlink($renamedFile); $page = HalcyonTestPage::create([ 'fileName' => 'Test', @@ -204,15 +211,27 @@ public function testUpdatePageRenameFileCase() 'markup' => '

I have an upper case, it should be lower

' ]); - $this->assertFileExists($targetFile); + // If the "renamed" file exists at this point we are on a case insensitive file system + // and this test will be unable to produce accurate results so skip it + // This test fails locally on Homestead on Mac OS when attempting to save the file after + // renaming it, most likely due to the case insensitive default file system on Mac OS + // Claims to fail because it can't create the file, and to check write permissions but + // actually fails due to "file_put_contents(/tests/fixtures/halcyon/themes/theme1/ + // pages/test.htm): Failed to open stream: Cannot allocate memory + if (file_exists($renamedFile)) { + $page->delete(); + $this->markTestSkipped("Test cannot successfully run on a case insensitive file system"); + } + + $this->assertFileExists($originalFile); $page->fileName = 'test'; $page->save(); - $newTargetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/test.htm'; - $this->assertFileExists($newTargetFile); + $this->assertFileExists($renamedFile); - @unlink($newTargetFile); + @unlink($originalFile); + @unlink($renamedFile); } public function testUpdateContentRenameExtension() diff --git a/tests/Html/BlockBuilderTest.php b/tests/Html/BlockBuilderTest.php index 51e82a096..bcc0b28d4 100644 --- a/tests/Html/BlockBuilderTest.php +++ b/tests/Html/BlockBuilderTest.php @@ -121,7 +121,7 @@ public function testPlaceholderBlock() . '', $this->Block->placeholder('test') ); - $this->assertNull($this->Block->get('test')); + $this->assertEquals('', $this->Block->get('test')); } public function testResetBlocks() @@ -137,7 +137,7 @@ public function testResetBlocks() $this->Block->reset(); - $this->assertNull($this->Block->get('test')); + $this->assertEquals('', $this->Block->get('test')); } public function testNestedBlocks() @@ -229,4 +229,13 @@ public function testContainBetweenBlocks() ); $this->assertEquals('In between', $content); } + + public function testGetBlock() + { + $result = $this->Block->get('non-existent-block'); + $this->assertNull($result); + + $result = $this->Block->get('non-existent-block', 'default value'); + $this->assertEquals('default value', $result); + } } diff --git a/tests/Mail/MailerTest.php b/tests/Mail/MailerTest.php index a12de01f1..e1f9d4fcc 100644 --- a/tests/Mail/MailerTest.php +++ b/tests/Mail/MailerTest.php @@ -1,6 +1,7 @@ assertArrayHasKey('user@domain.tld', $result); $this->assertEquals('Adam Person', $result['user@domain.tld']); + /* + * Array of email addresses without names + */ + $recipients = [ + 'admin@domain.tld', + 'single@address.com', + 'charles@barrington.com', + ]; + $result = self::callProtectedMethod($mailer, 'processRecipients', [$recipients]); + $this->assertCount(3, $result); + foreach ($recipients as $key => $value) { + $this->assertArrayHasKey($value, $result); + $this->assertEquals(null, $result[$value]); + } + /* * Array */ @@ -101,7 +117,7 @@ public function testProcessRecipients() protected function makeMailer() { - return new Mailer(new FactoryMailerTest, new SwiftMailerTest, new DispatcherMailerTest); + return new Mailer("TestMailer", new FactoryMailerTest, new ArrayTransport, new DispatcherMailerTest); } } @@ -118,10 +134,3 @@ public function __construct() { } } - -class SwiftMailerTest extends \Swift_Mailer -{ - public function __construct() - { - } -} diff --git a/tests/Parse/ArrayFileTest.php b/tests/Parse/ArrayFileTest.php new file mode 100644 index 000000000..bb7a85090 --- /dev/null +++ b/tests/Parse/ArrayFileTest.php @@ -0,0 +1,845 @@ +assertInstanceOf(ArrayFile::class, $arrayFile); + + $ast = $arrayFile->getAst(); + + $this->assertTrue(isset($ast[0]->expr->items[0]->key->value)); + $this->assertEquals('debug', $ast[0]->expr->items[0]->key->value); + } + + public function testWriteFile() + { + $filePath = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php'; + + $arrayFile = ArrayFile::open($filePath); + $arrayFile->write($tmpFile); + + $result = include $tmpFile; + $this->assertArrayHasKey('connections', $result); + $this->assertArrayHasKey('sqlite', $result['connections']); + $this->assertArrayHasKey('driver', $result['connections']['sqlite']); + $this->assertEquals('sqlite', $result['connections']['sqlite']['driver']); + + unlink($tmpFile); + } + + public function testWriteFileWithUpdates() + { + $filePath = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php'; + + $arrayFile = ArrayFile::open($filePath); + $arrayFile->set('connections.sqlite.driver', 'winter'); + $arrayFile->write($tmpFile); + + $result = include $tmpFile; + $this->assertArrayHasKey('connections', $result); + $this->assertArrayHasKey('sqlite', $result['connections']); + $this->assertArrayHasKey('driver', $result['connections']['sqlite']); + $this->assertEquals('winter', $result['connections']['sqlite']['driver']); + + unlink($tmpFile); + } + + public function testWriteFileWithUpdatesArray() + { + $filePath = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php'; + + $arrayFile = ArrayFile::open($filePath); + $arrayFile->set([ + 'connections.sqlite.driver' => 'winter', + 'connections.sqlite.prefix' => 'test', + ]); + $arrayFile->write($tmpFile); + + $result = include $tmpFile; + $this->assertArrayHasKey('connections', $result); + $this->assertArrayHasKey('sqlite', $result['connections']); + $this->assertArrayHasKey('driver', $result['connections']['sqlite']); + $this->assertEquals('winter', $result['connections']['sqlite']['driver']); + $this->assertEquals('test', $result['connections']['sqlite']['prefix']); + + unlink($tmpFile); + } + + public function testWriteEnvUpdates() + { + $filePath = __DIR__ . '/../fixtures/parse/arrayfile/env-config.php'; + $tmpFile = __DIR__ . '/../fixtures/parse/arrayfile/temp-array-file.php'; + + $arrayFile = ArrayFile::open($filePath); + $arrayFile->write($tmpFile); + + $result = include $tmpFile; + + $this->assertArrayHasKey('sample', $result); + $this->assertArrayHasKey('value', $result['sample']); + $this->assertArrayHasKey('no_default', $result['sample']); + $this->assertEquals('default', $result['sample']['value']); + $this->assertNull($result['sample']['no_default']); + + $arrayFile->set([ + 'sample.value' => 'winter', + 'sample.no_default' => 'test', + ]); + $arrayFile->write($tmpFile); + + $result = include $tmpFile; + + $this->assertArrayHasKey('sample', $result); + $this->assertArrayHasKey('value', $result['sample']); + $this->assertArrayHasKey('no_default', $result['sample']); + $this->assertEquals('winter', $result['sample']['value']); + $this->assertEquals('test', $result['sample']['no_default']); + + unlink($tmpFile); + } + + public function testCasting() + { + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $result = eval('?>' . $arrayFile->render()); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('url', $result); + $this->assertEquals('http://localhost', $result['url']); + + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $arrayFile->set('url', false); + $result = eval('?>' . $arrayFile->render()); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('url', $result); + $this->assertFalse($result['url']); + + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $arrayFile->set('url', 1234); + $result = eval('?>' . $arrayFile->render()); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('url', $result); + $this->assertIsInt($result['url']); + } + + public function testRender() + { + /* + * Rewrite a single level string + */ + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $arrayFile->set('url', 'https://wintercms.com'); + $result = eval('?>' . $arrayFile->render()); + + $this->assertTrue(is_array($result)); + $this->assertArrayHasKey('url', $result); + $this->assertEquals('https://wintercms.com', $result['url']); + + /* + * Rewrite a second level string + */ + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $arrayFile->set('memcached.host', '69.69.69.69'); + $result = eval('?>' . $arrayFile->render()); + + $this->assertArrayHasKey('memcached', $result); + $this->assertArrayHasKey('host', $result['memcached']); + $this->assertEquals('69.69.69.69', $result['memcached']['host']); + + /* + * Rewrite a third level string + */ + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $arrayFile->set('connections.mysql.host', '127.0.0.1'); + $result = eval('?>' . $arrayFile->render()); + + $this->assertArrayHasKey('connections', $result); + $this->assertArrayHasKey('mysql', $result['connections']); + $this->assertArrayHasKey('host', $result['connections']['mysql']); + $this->assertEquals('127.0.0.1', $result['connections']['mysql']['host']); + + /*un- + * Test alternative quoting + */ + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $arrayFile->set('timezone', 'The Fifth Dimension') + ->set('timezoneAgain', 'The "Sixth" Dimension'); + $result = eval('?>' . $arrayFile->render()); + + $this->assertArrayHasKey('timezone', $result); + $this->assertArrayHasKey('timezoneAgain', $result); + $this->assertEquals('The Fifth Dimension', $result['timezone']); + $this->assertEquals('The "Sixth" Dimension', $result['timezoneAgain']); + + /* + * Rewrite a boolean + */ + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $arrayFile->set('debug', false) + ->set('debugAgain', true) + ->set('bullyIan', true) + ->set('booLeeIan', false) + ->set('memcached.weight', false) + ->set('connections.pgsql.password', true); + + $result = eval('?>' . $arrayFile->render()); + + $this->assertArrayHasKey('debug', $result); + $this->assertArrayHasKey('debugAgain', $result); + $this->assertArrayHasKey('bullyIan', $result); + $this->assertArrayHasKey('booLeeIan', $result); + $this->assertFalse($result['debug']); + $this->assertTrue($result['debugAgain']); + $this->assertTrue($result['bullyIan']); + $this->assertFalse($result['booLeeIan']); + + $this->assertArrayHasKey('memcached', $result); + $this->assertArrayHasKey('weight', $result['memcached']); + $this->assertFalse($result['memcached']['weight']); + + $this->assertArrayHasKey('connections', $result); + $this->assertArrayHasKey('pgsql', $result['connections']); + $this->assertArrayHasKey('password', $result['connections']['pgsql']); + $this->assertTrue($result['connections']['pgsql']['password']); + $this->assertEquals('', $result['connections']['sqlsrv']['password']); + + /* + * Rewrite an integer + */ + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'); + $arrayFile->set('aNumber', 69); + $result = eval('?>' . $arrayFile->render()); + + $this->assertArrayHasKey('aNumber', $result); + $this->assertEquals(69, $result['aNumber']); + } + + public function testConfigInvalid() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('ArrayFiles must start with a return statement'); + + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/invalid.php'); + } + + public function testConfigImports() + { + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/import.php'); + + $expected = << Response::HTTP_OK, + 'bar' => Response::HTTP_I_AM_A_TEAPOT, +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testConfigImportsUpdating() + { + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/import.php'); + $arrayFile->set('foo', $arrayFile->constant('Response::HTTP_CONFLICT')); + + $expected = << Response::HTTP_CONFLICT, + 'bar' => Response::HTTP_I_AM_A_TEAPOT, +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testConfigExpression() + { + $arrayFile = ArrayFile::open(__DIR__ . '/../fixtures/parse/arrayfile/expression.php'); + + $expected = << \$bar, +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testReadCreateFile() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + + $this->assertFalse(file_exists($file)); + + $arrayFile = ArrayFile::open($file); + + $this->assertInstanceOf(ArrayFile::class, $arrayFile); + + $arrayFile->write(); + + $this->assertTrue(file_exists($file)); + $this->assertEquals(sprintf('set('w.i.n.t.e.r', 'cms'); + + $result = eval('?>' . $arrayFile->render()); + + $this->assertArrayHasKey('w', $result); + $this->assertArrayHasKey('i', $result['w']); + $this->assertArrayHasKey('n', $result['w']['i']); + $this->assertArrayHasKey('t', $result['w']['i']['n']); + $this->assertArrayHasKey('e', $result['w']['i']['n']['t']); + $this->assertArrayHasKey('r', $result['w']['i']['n']['t']['e']); + $this->assertEquals('cms', $result['w']['i']['n']['t']['e']['r']); + } + + public function testWriteDotNotationMixedCase() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + $arrayFile->set('w.0.n.1.e.2', 'cms'); + + $result = eval('?>' . $arrayFile->render()); + + $this->assertArrayHasKey('w', $result); + $this->assertArrayHasKey(0, $result['w']); + $this->assertArrayHasKey('n', $result['w'][0]); + $this->assertArrayHasKey(1, $result['w'][0]['n']); + $this->assertArrayHasKey('e', $result['w'][0]['n'][1]); + $this->assertArrayHasKey(2, $result['w'][0]['n'][1]['e']); + $this->assertEquals('cms', $result['w'][0]['n'][1]['e'][2]); + } + + public function testWriteDotNotationMultiple() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + $arrayFile->set('w.i.n.t.e.r', 'Winter CMS'); + $arrayFile->set('w.i.n.b', 'is'); + $arrayFile->set('w.i.n.t.a', 'very'); + $arrayFile->set('w.i.n.c.l', 'good'); + $arrayFile->set('w.i.n.c.e', 'and'); + $arrayFile->set('w.i.n.c.f', 'awesome'); + $arrayFile->set('w.i.n.g', 'for'); + $arrayFile->set('w.i.2.g', 'development'); + + $arrayFile->write(); + + $contents = file_get_contents($file); + + $expected = << [ + 'i' => [ + 'n' => [ + 't' => [ + 'e' => [ + 'r' => 'Winter CMS', + ], + 'a' => 'very', + ], + 'b' => 'is', + 'c' => [ + 'l' => 'good', + 'e' => 'and', + 'f' => 'awesome', + ], + 'g' => 'for', + ], + 2 => [ + 'g' => 'development', + ], + ], + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $contents); + + unlink($file); + } + + public function testWriteDotDuplicateIntKeys() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + $arrayFile->set([ + 'w.i.n.t.e.r' => 'Winter CMS', + 'w.i.2.g' => 'development', + ]); + $arrayFile->set('w.i.2.g', 'development'); + + $arrayFile->write(); + + $contents = file_get_contents($file); + + $expected = << [ + 'i' => [ + 'n' => [ + 't' => [ + 'e' => [ + 'r' => 'Winter CMS', + ], + ], + ], + 2 => [ + 'g' => 'development', + ], + ], + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $contents); + + unlink($file); + } + + public function testWriteIllegalOffset() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $this->expectException(\Winter\Storm\Exception\SystemException::class); + + $arrayFile->set([ + 'w.i.n.t.e.r' => 'Winter CMS', + 'w.i.n.t.e.r.2' => 'test', + ]); + } + + public function testThrowExceptionIfMissing() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/missing.php'; + + $this->expectException(\InvalidArgumentException::class); + + $arrayFile = ArrayFile::open($file, true); + } + + public function testSetArray() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'w' => [ + 'i' => 'n', + 't' => [ + 'e', + 'r' + ] + ] + ]); + + $expected = << [ + 'i' => 'n', + 't' => [ + 'e', + 'r', + ], + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testSetNumericArray() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'winter' => [ + 1 => 'a', + 2 => 'b', + ], + 'cms' => [ + 0 => 'a', + 1 => 'b' + ] + ]); + + $expected = << [ + 1 => 'a', + 2 => 'b', + ], + 'cms' => [ + 'a', + 'b', + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testWriteConstCall() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'curl_port' => $arrayFile->constant('CURLOPT_PORT') + ]); + + $arrayFile->set([ + 'curl_return' => new \Winter\Storm\Parse\PHP\PHPConstant('CURLOPT_RETURNTRANSFER') + ]); + + $expected = << CURLOPT_PORT, + 'curl_return' => CURLOPT_RETURNTRANSFER, +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testWriteArrayFunctionsAndConstCall() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'path.to.config' => [ + 'test' => $arrayFile->function('env', ['TEST_KEY', 'default']), + 'details' => [ + 'test1', + 'test2', + 'additional' => [ + $arrayFile->constant('\Winter\Storm\Parse\PHP\ArrayFile::SORT_ASC'), + $arrayFile->constant('\Winter\Storm\Parse\PHP\ArrayFile::SORT_DESC') + ] + ] + ] + ]); + + $expected = << [ + 'to' => [ + 'config' => [ + 'test' => env('TEST_KEY', 'default'), + 'details' => [ + 'test1', + 'test2', + 'additional' => [ + \Winter\Storm\Parse\PHP\ArrayFile::SORT_ASC, + \Winter\Storm\Parse\PHP\ArrayFile::SORT_DESC, + ], + ], + ], + ], + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testWriteFunctionCall() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'key' => $arrayFile->function('env', ['KEY_A', true]) + ]); + + $arrayFile->set([ + 'key2' => new \Winter\Storm\Parse\PHP\PHPFunction('nl2br', ['KEY_B', false]) + ]); + + $expected = << env('KEY_A', true), + 'key2' => nl2br('KEY_B', false), +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testWriteFunctionCallOverwrite() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'key' => $arrayFile->function('env', ['KEY_A', true]) + ]); + + $arrayFile->set([ + 'key' => new \Winter\Storm\Parse\PHP\PHPFunction('nl2br', ['KEY_B', false]) + ]); + + $expected = << nl2br('KEY_B', false), +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testInsertNull() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'key' => $arrayFile->function('env', ['KEY_A', null]), + 'key2' => null + ]); + + $expected = << env('KEY_A', null), + 'key2' => null, +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testSortAsc() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'b.b' => 'b', + 'b.a' => 'a', + 'a.a.b' => 'b', + 'a.a.a' => 'a', + 'a.c' => 'c', + 'a.b' => 'b', + ]); + + $arrayFile->sort(); + + $expected = << [ + 'a' => [ + 'a' => 'a', + 'b' => 'b', + ], + 'b' => 'b', + 'c' => 'c', + ], + 'b' => [ + 'a' => 'a', + 'b' => 'b', + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + + public function testSortDesc() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'b.a' => 'a', + 'a.a.a' => 'a', + 'a.a.b' => 'b', + 'a.b' => 'b', + 'a.c' => 'c', + 'b.b' => 'b', + ]); + + $arrayFile->sort(ArrayFile::SORT_DESC); + + $expected = << [ + 'b' => 'b', + 'a' => 'a', + ], + 'a' => [ + 'c' => 'c', + 'b' => 'b', + 'a' => [ + 'b' => 'b', + 'a' => 'a', + ], + ], +]; + +PHP; + + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testSortUsort() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/empty.php'; + $arrayFile = ArrayFile::open($file); + + $arrayFile->set([ + 'a' => 'a', + 'b' => 'b' + ]); + + $arrayFile->sort(function ($a, $b) { + static $i; + if (!isset($i)) { + $i = 1; + } + return $i--; + }); + + $expected = << 'b', + 'a' => 'a', +]; + +PHP; + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testIncludeFormatting() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/include.php'; + $arrayFile = ArrayFile::open($file); + + $expected = << array_merge(include(__DIR__ . '/sample-array-file.php'), [ + 'bar' => 'foo', + ]), + 'bar' => 'foo', +]; + +PHP; + $this->assertEquals(str_replace("\r", '', $expected), $arrayFile->render()); + } + + public function testEmptyNewLines() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/sample-array-file.php'; + $arrayFile = ArrayFile::open($file); + + preg_match('/^\s+$/m', $arrayFile->render(), $matches); + + $this->assertEmpty($matches); + } + + public function testNestedComments() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/nested-comments.php'; + $arrayFile = ArrayFile::open($file); + + $code = $arrayFile->render(); + + $this->assertStringContainsString(str_repeat(' ', 8) . '|', $code); + $this->assertStringNotContainsString(str_repeat(' ', 12) . '|', $code); + } + + public function testSingleLineComment() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/single-line-comments.php'; + $arrayFile = ArrayFile::open($file); + + $this->assertEquals( + str_replace("\r", '', file_get_contents($file)), + str_replace("\r", '', $arrayFile->render()) + ); + } + + public function testSingleLineCommentSubItem() + { + $file = __DIR__ . '/../fixtures/parse/arrayfile/single-line-comments-subitem.php'; + $arrayFile = ArrayFile::open($file); + + $this->assertEquals( + str_replace("\r", '', file_get_contents($file)), + str_replace("\r", '', $arrayFile->render()) + ); + } +} diff --git a/tests/Parse/EnvFileTest.php b/tests/Parse/EnvFileTest.php new file mode 100644 index 000000000..48412ce36 --- /dev/null +++ b/tests/Parse/EnvFileTest.php @@ -0,0 +1,189 @@ +assertInstanceOf(EnvFile::class, $env); + + $arr = $env->getVariables(); + + $this->assertArrayHasKey('APP_URL', $arr); + $this->assertArrayHasKey('APP_KEY', $arr); + $this->assertArrayHasKey('MAIL_HOST', $arr); + $this->assertArrayHasKey('MAIL_DRIVER', $arr); + $this->assertArrayHasKey('ROUTES_CACHE', $arr); + $this->assertArrayNotHasKey('KEY_WITH_NO_VALUE', $arr); + + $this->assertEquals('http://localhost', $arr['APP_URL']); + $this->assertEquals('changeme', $arr['APP_KEY']); + $this->assertEquals('smtp.mailgun.org', $arr['MAIL_HOST']); + $this->assertEquals('smtp', $arr['MAIL_DRIVER']); + $this->assertEquals('false', $arr['ROUTES_CACHE']); + } + + public function testWriteFile() + { + $filePath = __DIR__ . '/../fixtures/parse/test.env'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; + + $env = EnvFile::open($filePath); + $env->write($tmpFile); + + $result = file_get_contents($tmpFile); + + $this->assertStringContainsString('APP_DEBUG=true', $result); + $this->assertStringContainsString('DB_USE_CONFIG_FOR_TESTING=false', $result); + $this->assertStringContainsString('MAIL_HOST="smtp.mailgun.org"', $result); + $this->assertStringContainsString('ROUTES_CACHE=false', $result); + $this->assertStringContainsString('ENABLE_CSRF=true', $result); + $this->assertStringContainsString('KEY_WITH_NO_VALUE', $result); + + unlink($tmpFile); + } + + public function testWriteFileWithUpdates() + { + $filePath = __DIR__ . '/../fixtures/parse/test.env'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; + + $env = EnvFile::open($filePath); + $env->set('APP_KEY', 'winter'); + $env->write($tmpFile); + + $result = file_get_contents($tmpFile); + + $this->assertStringContainsString('APP_DEBUG=true', $result); + $this->assertStringContainsString('APP_KEY="winter"', $result); + $this->assertStringContainsString('DB_USE_CONFIG_FOR_TESTING=false', $result); + $this->assertStringContainsString('MAIL_HOST="smtp.mailgun.org"', $result); + $this->assertStringContainsString('ROUTES_CACHE=false', $result); + $this->assertStringContainsString('ENABLE_CSRF=true', $result); + $this->assertStringContainsString('# HELLO WORLD', $result); + $this->assertStringContainsString('#ENV_TEST="wintercms"', $result); + $this->assertStringContainsString('KEY_WITH_NO_VALUE', $result); + + unlink($tmpFile); + } + + public function testWriteFileWithUpdatesArray() + { + $filePath = __DIR__ . '/../fixtures/parse/test.env'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; + + $env = EnvFile::open($filePath); + $env->set([ + 'APP_KEY' => 'winter', + 'ROUTES_CACHE' => 'winter', + ]); + $env->write($tmpFile); + + $result = file_get_contents($tmpFile); + + $this->assertStringContainsString('APP_DEBUG=true', $result); + $this->assertStringContainsString('APP_KEY="winter"', $result); + $this->assertStringContainsString('DB_USE_CONFIG_FOR_TESTING=false', $result); + $this->assertStringContainsString('MAIL_HOST="smtp.mailgun.org"', $result); + $this->assertStringContainsString('ROUTES_CACHE="winter"', $result); + $this->assertStringContainsString('ENABLE_CSRF=true', $result); + $this->assertStringContainsString('# HELLO WORLD', $result); + $this->assertStringContainsString('#ENV_TEST="wintercms"', $result); + $this->assertStringContainsString('KEY_WITH_NO_VALUE', $result); + + unlink($tmpFile); + } + + public function testValueFormats() + { + $envFile = new EnvFile(''); + $cases = [ + 'APP_DEBUG=true' => [ + 'variable' => 'APP_DEBUG', + 'value' => true, + ], + 'APP_URL="https://localhost"' => [ + 'variable' => 'APP_URL', + 'value' => "https://localhost", + ], + 'DB_CONNECTION="mysql"' => [ + 'variable' => 'DB_CONNECTION', + 'value' => "mysql", + ], + 'DB_DATABASE="data#base"' => [ + 'variable' => 'DB_DATABASE', + 'value' => "data#base", + ], + 'DB_USERNAME="teal\\\'c"' => [ + 'variable' => 'DB_USERNAME', + 'value' => "teal\'c", + ], + 'DB_PASSWORD="test\\"quotes\\\'test"' => [ + 'variable' => 'DB_PASSWORD', + 'value' => "test\"quotes\'test", + ], + 'DB_PORT=3306' => [ + 'variable' => 'DB_PORT', + 'value' => 3306, + ], + ]; + + foreach ($cases as $output => $config) { + $envFile->set($config['variable'], $config['value']); + $this->assertStringContainsString($output, $envFile->render()); + } + } + + public function testCasting() + { + $filePath = __DIR__ . '/../fixtures/parse/test.env'; + $tmpFile = __DIR__ . '/../fixtures/parse/temp-test.env'; + + $env = EnvFile::open($filePath); + $env->set(['APP_KEY' => 'winter']); + $env->write($tmpFile); + + $result = file_get_contents($tmpFile); + $this->assertStringContainsString('APP_KEY="winter"', $result); + + $env->set(['APP_KEY' => '123']); + $env->write($tmpFile); + + $result = file_get_contents($tmpFile); + $this->assertStringContainsString('APP_KEY=123', $result); + + $env->set(['APP_KEY' => true]); + $env->write($tmpFile); + + $result = file_get_contents($tmpFile); + $this->assertStringContainsString('APP_KEY=true', $result); + + $env->set(['APP_KEY' => false]); + $env->write($tmpFile); + + $result = file_get_contents($tmpFile); + $this->assertStringContainsString('APP_KEY=false', $result); + + $env->set(['APP_KEY' => null]); + $env->write($tmpFile); + + $result = file_get_contents($tmpFile); + $this->assertStringContainsString('APP_KEY=null', $result); + + unlink($tmpFile); + } + + public function testRender() + { + $filePath = __DIR__ . '/../fixtures/parse/test.env'; + + $env = EnvFile::open($filePath); + + $this->assertEquals(file_get_contents($filePath), $env->render()); + } +} diff --git a/tests/Parse/SyntaxFieldParserTest.php b/tests/Parse/SyntaxFieldParserTest.php index 4f5915d29..a61808f96 100644 --- a/tests/Parse/SyntaxFieldParserTest.php +++ b/tests/Parse/SyntaxFieldParserTest.php @@ -256,13 +256,13 @@ public function testParseRepeater() public function testProcessTag() { - $parser = new FieldParser; $content = ''; $content .= '{text name="websiteName" label="Website Name" size="large"}{/text}'.PHP_EOL; $content .= '{text name="blogName" label="Blog Name" color="re\"d"}WinterCMS{/text}'.PHP_EOL; $content .= '{text name="storeName" label="Store Name" shape="circle"}{/text}'; $content .= '{text label="Unnamed" distance="400m"}Foobar{/text}'; $content .= '{foobar name="nullName" label="Valid tag, not searched by this test"}{/foobar}'; + $parser = new FieldParser($content); list($tags, $fields) = self::callProtectedMethod($parser, 'processTags', [$content]); $unnamedTag = md5('{text label="Unnamed" distance="400m"}Foobar{/text}'); @@ -328,11 +328,11 @@ public function testProcessTag() public function testProcessTagsRegex() { - $parser = new FieldParser; $content = ''; $content .= '{text name="websiteName" label="Website Name"}{/text}'.PHP_EOL; $content .= '{text name="blogName" label="Blog Name"}WinterCMS{/text}'.PHP_EOL; $content .= '{text name="storeName" label="Store Name"}{/text}'; + $parser = new FieldParser($content); $result = self::callProtectedMethod($parser, 'processTagsRegex', [$content, ['text']]); $this->assertArrayHasKey(0, $result[2]); @@ -346,8 +346,8 @@ public function testProcessTagsRegex() public function testProcessParamsRegex() { - $parser = new FieldParser; $content = 'name="test" comment="This is a test"'; + $parser = new FieldParser($content); $result = self::callProtectedMethod($parser, 'processParamsRegex', [$content]); $this->assertArrayHasKey(0, $result[1]); diff --git a/tests/Parse/YamlTest.php b/tests/Parse/YamlTest.php index 9a73070da..d8ff21f2d 100644 --- a/tests/Parse/YamlTest.php +++ b/tests/Parse/YamlTest.php @@ -1,12 +1,17 @@ markTestSkipped("YAML processing should only be for cleaning up bad YAML."); + $parser = new YamlParser; $yaml = $parser->parse(file_get_contents(dirname(__DIR__) . '/fixtures/yaml/test.yaml')); @@ -27,6 +32,9 @@ public function testParseWithoutProcessor() public function testParseWithPreProcessor() { + // @TODO: Rethink processing logic + $this->markTestSkipped("YAML processing should only be for cleaning up bad YAML."); + $parser = new YamlParser; $parser->setProcessor(new UppercaseYamlProcessor); $yaml = $parser->parse(file_get_contents(dirname(__DIR__) . '/fixtures/yaml/test.yaml')); @@ -49,6 +57,9 @@ public function testParseWithPreProcessor() public function testParseWithPreProcessorTemporarily() { + // @TODO: Rethink processing logic + $this->markTestSkipped("YAML processing should only be for cleaning up bad YAML."); + $parser = new YamlParser; $yaml = $parser->withProcessor(new UppercaseYamlProcessor, function ($yaml) { return $yaml->parse(file_get_contents(dirname(__DIR__) . '/fixtures/yaml/test.yaml')); @@ -71,6 +82,9 @@ public function testParseWithPreProcessorTemporarily() public function testParseWithPostProcessor() { + // @TODO: Rethink processing logic + $this->markTestSkipped("YAML processing should only be for cleaning up bad YAML."); + $parser = new YamlParser; $parser->setProcessor(new ObjectYamlProcessor); $yaml = $parser->parse(file_get_contents(dirname(__DIR__) . '/fixtures/yaml/test.yaml')); @@ -89,36 +103,241 @@ public function testParseWithPostProcessor() ], ], $yaml->test); } + + public function testRenderWithoutProcessor() + { + // @TODO: Rethink processing logic + $this->markTestSkipped("YAML processing should only be for cleaning up bad YAML."); + + $parser = new YamlParser; + + $yaml = $parser->render([ + '1.0.0' => [ + 'First version', + 'some_update_file.php', + ], + '1.0.1' => [ + 'Second version', + ], + 'test' => [ + 'String-based key', + ], + 'test two' => [ + 'String-based key with a space', + ], + ]); + + $this->assertIsString($yaml); + $this->assertEquals( + "1.0.0:\n" . + " - 'First version'\n" . + " - some_update_file.php\n" . + "1.0.1:\n" . + " - 'Second version'\n" . + "test:\n" . + " - 'String-based key'\n" . + "'test two':\n" . + " - 'String-based key with a space'\n", + $yaml + ); + } + + public function testRenderWithPreProcessor() + { + // @TODO: Rethink processing logic + $this->markTestSkipped("YAML processing should only be for cleaning up bad YAML."); + + $parser = new YamlParser; + + $parser->setProcessor(new UppercaseKeysProcessor); + $yaml = $parser->render([ + '1.0.0' => [ + 'First version', + 'some_update_file.php', + ], + '1.0.1' => [ + 'Second version', + ], + 'test' => [ + 'String-based key', + ], + 'test two' => [ + 'String-based key with a space', + ], + ]); + $parser->removeProcessor(); + + $this->assertIsString($yaml); + $this->assertEquals( + "1.0.0:\n" . + " - 'First version'\n" . + " - some_update_file.php\n" . + "1.0.1:\n" . + " - 'Second version'\n" . + "TEST:\n" . + " - 'String-based key'\n" . + "'TEST TWO':\n" . + " - 'String-based key with a space'\n", + $yaml + ); + } + + public function testRenderWithPreAndPostProcessor() + { + // @TODO: Rethink processing logic + $this->markTestSkipped("YAML processing should only be for cleaning up bad YAML."); + + $parser = new YamlParser; + + $parser->setProcessor(new QuotedUpperKeysProcessor); + $yaml = $parser->render([ + '1.0.0' => [ + 'First version', + 'some_update_file.php', + ], + '1.0.1' => [ + 'Second version', + ], + 'test' => [ + 'String-based key', + ], + 'test two' => [ + 'String-based key with a space', + ], + ]); + $parser->removeProcessor(); + + $this->assertIsString($yaml); + $this->assertEquals( + "'1.0.0':\n" . + " - 'First version'\n" . + " - some_update_file.php\n" . + "'1.0.1':\n" . + " - 'Second version'\n" . + "'TEST':\n" . + " - 'String-based key'\n" . + "'TEST TWO':\n" . + " - 'String-based key with a space'\n", + $yaml + ); + } + + public function testSymfony3YamlFile() + { + // This YAML file should not be parseable by default + $this->expectException(ParseException::class); + + $parser = new YamlParser; + $parser->parse(file_get_contents(dirname(__DIR__) . '/fixtures/yaml/symfony3.yaml')); + } + + public function testSymfony3YamlFileWithProcessor() + { + $parser = new YamlParser; + $parser->setProcessor(new Symfony3Processor); + $yaml = $parser->parse(file_get_contents(dirname(__DIR__) . '/fixtures/yaml/symfony3.yaml')); + + $this->assertEquals([ + // Form config file + 'form' => [ + // field options array, unquoted keys & values + 'options' => [ + '0.1' => '0.1', + '0.2' => '0.2', + ], + + // field options array, unquoted keys + 'options2' => [ + '0.1' => '0.1', + '0.2' => '0.2', + ], + + // Aligned colons + 'options3' => [ + '0.1' => '0.1', + '0.2' => '0.2', + ], + ], + + // version.yaml file + 'updates' => [ + '1.0.1' => 'First version of Plugin', + '1.0.2' => [ + 'Create plugin tables', + 'create_plugin_table.php', + ], + '1.1' => [ + 'Add new component', + 'create_component_table.php', + ], + '1.1.1' => [ + 'Update column property', + 'update_column_property.php', + ], + ], + ], $yaml['numeric_keys_not_supported']); + } } /** - * Test pre-processor + * Test parse pre-processor */ -class UppercaseYamlProcessor implements YamlProcessor +class UppercaseYamlProcessor extends YamlProcessor { public function preprocess($text) { return strtoupper($text); } +} +/** + * Test parse post-processor + */ +class ObjectYamlProcessor extends YamlProcessor +{ public function process($parsed) { - return $parsed; + return (object) $parsed; } } /** - * Test post-processor + * Test render pre-processor */ -class ObjectYamlProcessor implements YamlProcessor +class UppercaseKeysProcessor extends YamlProcessor { - public function preprocess($text) + public function prerender($data) + { + $processed = []; + + foreach ($data as $key => $value) { + $processed[strtoupper($key)] = $value; + } + + return $processed; + } +} + +/** + * Test render pre-and-post-processor + */ +class QuotedUpperKeysProcessor extends YamlProcessor +{ + public function prerender($data) { - return $text; + $processed = []; + + foreach ($data as $key => $value) { + $processed[strtoupper($key)] = $value; + } + + return $processed; } - public function process($parsed) + public function render($yaml) { - return (object) $parsed; + return preg_replace_callback('/^\s*([\'"]{0}[^\'"\n\r:]+[\'"]{0})\s*:\s*$/m', function ($matches) { + return "'" . trim($matches[1]) . "':"; + }, $yaml); } } diff --git a/tests/Router/RoutingUrlGeneratorTest.php b/tests/Router/RoutingUrlGeneratorTest.php index b8277f1e1..e3d55892d 100644 --- a/tests/Router/RoutingUrlGeneratorTest.php +++ b/tests/Router/RoutingUrlGeneratorTest.php @@ -1,11 +1,15 @@ assertSame('/foo/routable', $url->route('routable', [$model], false)); } + public function testRoutableInterfaceRoutingWithCustomBindingField() + { + $url = new UrlGenerator( + $routes = new RouteCollection, + Request::create('http://www.foo.com/') + ); + + $route = new Route(['GET'], 'foo/{bar:slug}', ['as' => 'routable']); + $routes->add($route); + + $model = new RoutableInterfaceStub; + $model->key = 'routable'; + + $this->assertSame('/foo/test-slug', $url->route('routable', ['bar' => $model], false)); + $this->assertSame('/foo/test-slug', $url->route('routable', [$model], false)); + } + public function testRoutableInterfaceRoutingWithSingleParameter() { $url = new UrlGenerator( @@ -492,7 +513,7 @@ public function testHttpsRoutesWithDomains() public function testRoutesWithDomainsThroughProxy() { - Request::setTrustedProxies(['10.0.0.1'], SymfonyRequest::HEADER_X_FORWARDED_ALL); + Request::setTrustedProxies(['10.0.0.1'], SymfonyRequest::HEADER_X_FORWARDED_FOR | SymfonyRequest::HEADER_X_FORWARDED_HOST | SymfonyRequest::HEADER_X_FORWARDED_PORT | SymfonyRequest::HEADER_X_FORWARDED_PROTO); $url = new UrlGenerator( $routes = new RouteCollection, @@ -534,6 +555,61 @@ public function testUrlGenerationForControllersRequiresPassingOfRequiredParamete $this->assertSame('http://www.foo.com:8080/foo?test=123', $url->route('foo', $parameters)); } + public function provideParametersAndExpectedMeaningfulExceptionMessages() + { + return [ + 'Missing parameters "one", "two" and "three"' => [ + [], + 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, two, three].', + ], + 'Missing parameters "two" and "three"' => [ + ['one' => '123'], + 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: two, three].', + ], + 'Missing parameters "one" and "three"' => [ + ['two' => '123'], + 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, three].', + ], + 'Missing parameters "one" and "two"' => [ + ['three' => '123'], + 'Missing required parameters for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameters: one, two].', + ], + 'Missing parameter "three"' => [ + ['one' => '123', 'two' => '123'], + 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: three].', + ], + 'Missing parameter "two"' => [ + ['one' => '123', 'three' => '123'], + 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: two].', + ], + 'Missing parameter "one"' => [ + ['two' => '123', 'three' => '123'], + 'Missing required parameter for [Route: foo] [URI: foo/{one}/{two}/{three}/{four?}] [Missing parameter: one].', + ], + ]; + } + + /** + * @dataProvider provideParametersAndExpectedMeaningfulExceptionMessages + */ + public function testUrlGenerationThrowsExceptionForMissingParametersWithMeaningfulMessage($parameters, $expectedMeaningfulExceptionMessage) + { + $this->expectException(UrlGenerationException::class); + $this->expectExceptionMessage($expectedMeaningfulExceptionMessage); + + $url = new UrlGenerator( + $routes = new RouteCollection, + Request::create('http://www.foo.com:8080/') + ); + + $route = new Route(['GET'], 'foo/{one}/{two}/{three}/{four?}', ['as' => 'foo', function () { + // + }]); + $routes->add($route); + + $url->route('foo', $parameters); + } + public function testForceRootUrl() { $url = new UrlGenerator( @@ -619,6 +695,28 @@ public function testSignedUrl() $this->assertFalse($url->hasValidSignature($request)); } + public function testSignedUrlImplicitModelBinding() + { + $url = new UrlGenerator( + $routes = new RouteCollection, + $request = Request::create('http://www.foo.com/') + ); + $url->setKeyResolver(function () { + return 'secret'; + }); + + $route = new Route(['GET'], 'foo/{user:uuid}', ['as' => 'foo', function () { + // + }]); + $routes->add($route); + + $user = new RoutingUrlGeneratorTestUser(['uuid' => '0231d4ac-e9e3-4452-a89a-4427cfb23c3e']); + + $request = Request::create($url->signedRoute('foo', $user)); + + $this->assertTrue($url->hasValidSignature($request)); + } + public function testSignedRelativeUrl() { $url = new UrlGenerator( @@ -645,54 +743,53 @@ public function testSignedRelativeUrl() $this->assertFalse($url->hasValidSignature($request, false)); } - // @TODO: Waiting for https://github.com/laravel/framework/commit/cd49e7e24a22251e97ca27224e08bf444d35a8a4 to be released - // public function testSignedUrlParameterCannotBeNamedSignature() - // { - // $url = new UrlGenerator( - // $routes = new RouteCollection, - // $request = Request::create('http://www.foo.com/') - // ); - // $url->setKeyResolver(function () { - // return 'secret'; - // }); - - // $route = new Route(['GET'], 'foo/{signature}', ['as' => 'foo', function () { - // // - // }]); - // $routes->add($route); - - // $this->expectException(InvalidArgumentException::class); - // $this->expectExceptionMessage('reserved'); - - // Request::create($url->signedRoute('foo', ['signature' => 'bar'])); - // } - - // @TODO: Waiting for https://github.com/laravel/framework/commit/cd49e7e24a22251e97ca27224e08bf444d35a8a4 to be released - // public function testSignedUrlParameterCannotBeNamedExpires() - // { - // $url = new UrlGenerator( - // $routes = new RouteCollection, - // $request = Request::create('http://www.foo.com/') - // ); - // $url->setKeyResolver(function () { - // return 'secret'; - // }); - - // $route = new Route(['GET'], 'foo/{expires}', ['as' => 'foo', function () { - // // - // }]); - // $routes->add($route); - - // $this->expectException(InvalidArgumentException::class); - // $this->expectExceptionMessage('reserved'); - - // Request::create($url->signedRoute('foo', ['expires' => 253402300799])); - // } + public function testSignedUrlParameterCannotBeNamedSignature() + { + $url = new UrlGenerator( + $routes = new RouteCollection, + $request = Request::create('http://www.foo.com/') + ); + $url->setKeyResolver(function () { + return 'secret'; + }); + + $route = new Route(['GET'], 'foo/{signature}', ['as' => 'foo', function () { + // + }]); + $routes->add($route); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('reserved'); + + Request::create($url->signedRoute('foo', ['signature' => 'bar'])); + } + + public function testSignedUrlParameterCannotBeNamedExpires() + { + $url = new UrlGenerator( + $routes = new RouteCollection, + $request = Request::create('http://www.foo.com/') + ); + $url->setKeyResolver(function () { + return 'secret'; + }); + + $route = new Route(['GET'], 'foo/{expires}', ['as' => 'foo', function () { + // + }]); + $routes->add($route); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('reserved'); + + Request::create($url->signedRoute('foo', ['expires' => 253402300799])); + } } class RoutableInterfaceStub implements UrlRoutable { public $key; + public $slug = 'test-slug'; public function getRouteKey() { @@ -704,7 +801,12 @@ public function getRouteKeyName() return 'key'; } - public function resolveRouteBinding($routeKey) + public function resolveRouteBinding($routeKey, $field = null) + { + // + } + + public function resolveChildRouteBinding($childType, $routeKey, $field = null) { // } @@ -717,3 +819,8 @@ public function __invoke() return 'hello'; } } + +class RoutingUrlGeneratorTestUser extends Model +{ + protected $fillable = ['uuid']; +} diff --git a/tests/Router/UrlGeneratorTest.php b/tests/Router/UrlGeneratorTest.php index 5ec7a9330..c4e69857e 100644 --- a/tests/Router/UrlGeneratorTest.php +++ b/tests/Router/UrlGeneratorTest.php @@ -537,4 +537,32 @@ public function testQueryArgsArrayMatchLaravel() ); } } + + public function testEncodedUrlInPathMatchLaravel() + { + $urlInPath = 'https://testUrlInPath/path?k1=v1&k2'; + $url = 'https://testdomain/' . rawurlencode($urlInPath); + + $generator = new \Winter\Storm\Router\UrlGenerator(new RouteCollection, Request::create($url)); + $baseGenerator = new \Illuminate\Routing\UrlGenerator(new RouteCollection, Request::create($url)); + + $this->assertEquals( + urldecode($baseGenerator->to($url)), + urldecode($generator->to($url)) + ); + } + + public function testDoublyEncodedUrlInPathMatchLaravel() + { + $urlInPath = 'https://testUrlInPath/path?k1=v1&k2'; + $url = 'https://testdomain/' . rawurlencode(rawurlencode($urlInPath)); + + $generator = new \Winter\Storm\Router\UrlGenerator(new RouteCollection, Request::create($url)); + $baseGenerator = new \Illuminate\Routing\UrlGenerator(new RouteCollection, Request::create($url)); + + $this->assertEquals( + urldecode($baseGenerator->to($url)), + urldecode($generator->to($url)) + ); + } } diff --git a/tests/Scaffold/ScaffoldBaseTest.php b/tests/Scaffold/ScaffoldBaseTest.php index 27cf22ba3..7abb4ba22 100644 --- a/tests/Scaffold/ScaffoldBaseTest.php +++ b/tests/Scaffold/ScaffoldBaseTest.php @@ -8,7 +8,7 @@ public function __construct() { } - protected function prepareVars() + protected function prepareVars(): array { return []; } diff --git a/tests/Support/ClassLoaderTest.php b/tests/Support/ClassLoaderTest.php index 827c5f709..9f1ffb23b 100644 --- a/tests/Support/ClassLoaderTest.php +++ b/tests/Support/ClassLoaderTest.php @@ -44,7 +44,7 @@ public function testAliases() ]); // Check that class identifies as both original and alias - $newInstance = new Winter\Plugin\Classes\TestClass; + $newInstance = new \Winter\Plugin\Classes\TestClass; $this->assertTrue($newInstance instanceof Winter\Plugin\Classes\TestClass); $this->assertTrue($newInstance instanceof OldOrg\Plugin\Classes\TestClass); diff --git a/tests/Support/EmitterTest.php b/tests/Support/EmitterTest.php index d01947cb3..a461bbabd 100644 --- a/tests/Support/EmitterTest.php +++ b/tests/Support/EmitterTest.php @@ -1,5 +1,7 @@ assertEquals('the quick brown fox jumped over the lazy dog', $result); } + + /** + * Test closure usage + */ + public function testTypedClosureListen() + { + $magic_value = false; + $dispatcher = $this->traitObject; + $dispatcher->bindEvent(function (EventTest $event) use (&$magic_value) { + $magic_value = true; + }); + $dispatcher->fireEvent('test.test'); + $this->assertFalse($magic_value); + $dispatcher->fireEvent(new EventTest); + $this->assertTrue($magic_value); + } + + public function testClosurePriorities() + { + $magic_value = 0; + $dispatcher = $this->traitObject; + + $dispatcher->bindEvent(function (EventTest $test) use (&$magic_value) { + $magic_value = 42; + }, 1); + $dispatcher->bindEvent(function (EventTest $test) use (&$magic_value) { + $magic_value = 1; + }, 2); + + $dispatcher->fireEvent(new EventTest()); + $this->assertEquals(42, $magic_value); + } + + public function testQueuedClosurePriorities() + { + $mock_queued_closure_should_match = $this->createMock(QueuedClosure::class); + $mock_queued_closure_should_match->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = 42; + }; + $mock_queued_closure_should_match->method('resolve')->willReturn($mock_queued_closure_should_match->closure); + + $mock_queued_closure_should_not_match = $this->createMock(QueuedClosure::class); + $mock_queued_closure_should_not_match->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = 2; + }; + $mock_queued_closure_should_not_match->method('resolve')->willReturn($mock_queued_closure_should_not_match->closure); + $dispatcher = $this->traitObject; + $magic_value = 0; + + // Test natural sorting without priority to the queued tasks to be queued. + $dispatcher->bindEvent($mock_queued_closure_should_not_match); + $dispatcher->bindEvent($mock_queued_closure_should_match); + $dispatcher->fireEvent(new EventTest()); + $this->assertEquals(42, $magic_value); + + // Test priority sorting for the queued tasks to be queued + $magic_value = 0; + $dispatcher->bindEvent($mock_queued_closure_should_match, 1); + $dispatcher->bindEvent($mock_queued_closure_should_not_match, 2); + $dispatcher->fireEvent(new EventTest()); + $this->assertEquals(42, $magic_value); + } + + /** + * Test whether the Emitter accepts a QueuedClosure + */ + public function testQueuedClosureListen() + { + $magic_value = false; + $mock_queued_closure = $this->createMock(QueuedClosure::class); + $mock_queued_closure->closure = function (EventTest $test) use (&$magic_value) { + $magic_value = true; + }; + $mock_queued_closure->method('resolve')->willReturn($mock_queued_closure->closure); + $dispatcher = $this->traitObject; + $dispatcher->bindEvent($mock_queued_closure); + $dispatcher->fireEvent(new EventTest()); + $this->assertTrue($magic_value); + } + + public function testClosureSerialization() + { + $emitter = new EmitterClass(); + $test = 'foobar'; + $emitter->bindEvent($test, function () use ($test) { + EmitterClass::$output = $test; + }); + $emitter->bindEvent(function (EventTest $event) use ($test) { + EmitterClass::$output = $test.$test; + }); + $serialized = serialize($emitter); + $unserialized = unserialize($serialized); + $unserialized->fireEvent($test); + $this->assertEquals($test, EmitterClass::$output); + + $unserialized->fireEvent(new EventTest()); + $this->assertEquals($test.$test, EmitterClass::$output); + } + + public function testNestedClosureSerialization() + { + $emitter = new EmitterClass(); + $test = 'foobar'; + $emitter->bindEvent($test, function () use ($test) { + EmitterClass::$output = function () use ($test) { + return $test; + }; + }); + + $serialized = serialize($emitter); + $unserialized = unserialize($serialized); + $unserialized->fireEvent($test); + + $closure = EmitterClass::$output; + $this->assertInstanceOf(Closure::class, $closure); + $this->assertEquals($test, $closure()); + } +} +class EmitterClass +{ + use \Winter\Storm\Support\Traits\Emitter; + + /** + * @var string $output used for keeping a testable variable as references don't survive serialisation + */ + public static $output; } diff --git a/tests/Support/ExtensionAndEmitterSerializationTest.php b/tests/Support/ExtensionAndEmitterSerializationTest.php new file mode 100644 index 000000000..abe331763 --- /dev/null +++ b/tests/Support/ExtensionAndEmitterSerializationTest.php @@ -0,0 +1,34 @@ +bindEvent($test, function () use ($test) { + ExtendableEmitter::$output = $test; + }); + }); + $instance = new ExtendableEmitter(); + $serialized = serialize($instance); + $unserialized = unserialize($serialized); + $unserialized->fireEvent($test); + $this->assertEquals($test, ExtendableEmitter::$output); + } +} + +class ExtendableEmitter extends Extendable +{ + use \Winter\Storm\Support\Traits\Emitter; + + /** + * @var string $output used for keeping a testable variable as references don't survive serialisation + */ + public static $output; +} diff --git a/tests/Support/MailFakeTest.php b/tests/Support/MailFakeTest.php index 73c99d5eb..144a1452c 100644 --- a/tests/Support/MailFakeTest.php +++ b/tests/Support/MailFakeTest.php @@ -1,21 +1,16 @@ andreturn('en/US'); + parent::setUp(); - // Mock Mail facade - if (!class_exists('Mail')) { - class_alias('\Winter\Storm\Support\Facades\Mail', 'Mail'); - } + App::shouldReceive('getLocale')->andReturn('en/US'); Mail::swap(new MailFake()); $this->recipient = 'fake@localhost'; diff --git a/tests/TestCase.php b/tests/TestCase.php index 8cf6767cd..6a59eba7d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -1,16 +1,27 @@ getBasePath()), function ($app) { + $app->bind( + \Winter\Storm\Foundation\Bootstrap\LoadConfiguration::class, + \Orchestra\Testbench\Bootstrap\LoadConfiguration::class + ); + + PackageManifest::swap($app, $this); + }); } protected static function callProtectedMethod($object, $name, $params = []) @@ -38,4 +49,78 @@ public static function assertFileNotExists(string $filename, string $message = ' Assert::assertFileNotExists($filename, $message); } + + /** + * Resolve application Console Kernel implementation. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function resolveApplicationConsoleKernel($app) + { + $app->singleton( + \Illuminate\Contracts\Console\Kernel::class, + \Winter\Storm\Foundation\Console\Kernel::class + ); + } + + /** + * Resolve application HTTP Kernel implementation. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function resolveApplicationHttpKernel($app) + { + $app->singleton( + \Illuminate\Contracts\Http\Kernel::class, + \Winter\Storm\Foundation\Http\Kernel::class + ); + } + + /** + * Get package providers. + * + * @param \Illuminate\Foundation\Application $app + * + * @return array + */ + protected function getPackageProviders($app) + { + return [ + /* + * Laravel providers + */ + \Illuminate\Broadcasting\BroadcastServiceProvider::class, + \Illuminate\Bus\BusServiceProvider::class, + \Illuminate\Cache\CacheServiceProvider::class, + \Illuminate\Cookie\CookieServiceProvider::class, + \Illuminate\Encryption\EncryptionServiceProvider::class, + \Illuminate\Foundation\Providers\FoundationServiceProvider::class, + \Illuminate\Hashing\HashServiceProvider::class, + \Illuminate\Pagination\PaginationServiceProvider::class, + \Illuminate\Pipeline\PipelineServiceProvider::class, + \Illuminate\Queue\QueueServiceProvider::class, + \Illuminate\Session\SessionServiceProvider::class, + \Illuminate\View\ViewServiceProvider::class, + \Laravel\Tinker\TinkerServiceProvider::class, + + /* + * Winter Storm providers + */ + \Winter\Storm\Foundation\Providers\ConsoleSupportServiceProvider::class, + \Winter\Storm\Database\DatabaseServiceProvider::class, + \Winter\Storm\Halcyon\HalcyonServiceProvider::class, + \Winter\Storm\Filesystem\FilesystemServiceProvider::class, + \Winter\Storm\Parse\ParseServiceProvider::class, + \Winter\Storm\Html\HtmlServiceProvider::class, + \Winter\Storm\Html\UrlServiceProvider::class, + \Winter\Storm\Network\NetworkServiceProvider::class, + \Winter\Storm\Flash\FlashServiceProvider::class, + \Winter\Storm\Mail\MailServiceProvider::class, + \Winter\Storm\Argon\ArgonServiceProvider::class, + \Winter\Storm\Redis\RedisServiceProvider::class, + \Winter\Storm\Validation\ValidationServiceProvider::class, + ]; + } } diff --git a/tests/Translation/FileLoaderTest.php b/tests/Translation/FileLoaderTest.php new file mode 100644 index 000000000..6b9fa3491 --- /dev/null +++ b/tests/Translation/FileLoaderTest.php @@ -0,0 +1,90 @@ +shouldReceive('exists')->once()->with(__DIR__.'/en/foo.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/en/foo.php')->andReturn(['messages']); + + $this->assertEquals(['messages'], $loader->load('en', 'foo', null)); + } + + public function testLoadMethodWithNamespacesProperlyCallsLoader() + { + $loader = new FileLoader($files = m::mock(Filesystem::class), __DIR__); + $files->shouldReceive('exists')->once()->with('bar/en/foo.php')->andReturn(true); + $files->shouldReceive('exists')->once()->with(__DIR__.'/en/namespace/foo.php')->andReturn(false); + $files->shouldReceive('getRequire')->once()->with('bar/en/foo.php')->andReturn(['foo' => 'bar']); + $loader->addNamespace('namespace', 'bar'); + + $this->assertEquals(['foo' => 'bar'], $loader->load('en', 'foo', 'namespace')); + } + + public function testLoadMethodWithNamespacesProperlyCallsLoaderAndLoadsLocalOverrides() + { + $loader = new FileLoader($files = m::mock(Filesystem::class), __DIR__); + $files->shouldReceive('exists')->once()->with('bar/en/foo.php')->andReturn(true); + $files->shouldReceive('exists')->once()->with(__DIR__.'/en/namespace/foo.php')->andReturn(true); + $files->shouldReceive('getRequire')->once()->with('bar/en/foo.php')->andReturn(['foo' => 'bar']); + $files->shouldReceive('getRequire')->once()->with(__DIR__.'/en/namespace/foo.php')->andReturn(['foo' => 'override', 'baz' => 'boom']); + $loader->addNamespace('namespace', 'bar'); + + $this->assertEquals(['foo' => 'override', 'baz' => 'boom'], $loader->load('en', 'foo', 'namespace')); + } + + public function testEmptyArraysReturnedWhenFilesDontExist() + { + $loader = new FileLoader($files = m::mock(Filesystem::class), __DIR__); + $files->shouldReceive('exists')->once()->with(__DIR__.'/en/foo.php')->andReturn(false); + $files->shouldReceive('getRequire')->never(); + + $this->assertEquals([], $loader->load('en', 'foo', null)); + } + + public function testEmptyArraysReturnedWhenFilesDontExistForNamespacedItems() + { + $loader = new FileLoader($files = m::mock(Filesystem::class), __DIR__); + $files->shouldReceive('getRequire')->never(); + + $this->assertEquals([], $loader->load('en', 'foo', 'bar')); + } + + public function testLoadMethodForJSONProperlyCallsLoader() + { + $loader = new FileLoader($files = m::mock(Filesystem::class), __DIR__); + $files->shouldReceive('exists')->once()->with(__DIR__.'/en.json')->andReturn(true); + $files->shouldReceive('get')->once()->with(__DIR__.'/en.json')->andReturn('{"foo":"bar"}'); + + $this->assertEquals(['foo' => 'bar'], $loader->load('en', '*', '*')); + } + + public function testLoadMethodForJSONProperlyCallsLoaderForMultiplePaths() + { + $loader = new FileLoader($files = m::mock(Filesystem::class), __DIR__); + $loader->addJsonPath(__DIR__.'/another'); + + $files->shouldReceive('exists')->once()->with(__DIR__.'/en.json')->andReturn(true); + $files->shouldReceive('exists')->once()->with(__DIR__.'/another/en.json')->andReturn(true); + $files->shouldReceive('get')->once()->with(__DIR__.'/en.json')->andReturn('{"foo":"bar"}'); + $files->shouldReceive('get')->once()->with(__DIR__.'/another/en.json')->andReturn('{"foo":"backagebar", "baz": "backagesplash"}'); + + $this->assertEquals(['foo' => 'bar', 'baz' => 'backagesplash'], $loader->load('en', '*', '*')); + } +} diff --git a/tests/Translation/TranslatorTest.php b/tests/Translation/TranslatorTest.php index a28e28b42..29b6f8cd5 100644 --- a/tests/Translation/TranslatorTest.php +++ b/tests/Translation/TranslatorTest.php @@ -1,10 +1,19 @@ addNamespace('winter.test', $path); $this->translator = $translator; } + protected function tearDown(): void + { + m::close(); + } + + protected function getLoader() + { + return m::mock(Loader::class); + } + + public function testHasMethodReturnsFalseWhenReturnedTranslationIsNull() + { + $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('bar'))->willReturn('foo'); + $this->assertFalse($t->has('foo', 'bar')); + + $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en', 'sp'])->getMock(); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('bar'))->willReturn('bar'); + $this->assertTrue($t->has('foo', 'bar')); + + $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('bar'), false)->willReturn('bar'); + $this->assertTrue($t->hasForLocale('foo', 'bar')); + + $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('bar'), false)->willReturn('foo'); + $this->assertFalse($t->hasForLocale('foo', 'bar')); + + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo', '*')->andReturn(['foo' => 'bar']); + $this->assertTrue($t->hasForLocale('foo')); + + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo', '*')->andReturn([]); + $this->assertFalse($t->hasForLocale('foo')); + } + + public function testGetMethodProperlyLoadsAndRetrievesItem() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['foo' => 'foo', 'baz' => 'breeze :foo', 'qux' => ['tree :foo', 'breeze :foo']]); + $this->assertEquals(['tree bar', 'breeze bar'], $t->get('foo::bar.qux', ['foo' => 'bar'], 'en')); + $this->assertSame('breeze bar', $t->get('foo::bar.baz', ['foo' => 'bar'], 'en')); + $this->assertSame('foo', $t->get('foo::bar.foo')); + } + + public function testGetMethodProperlyLoadsAndRetrievesArrayItem() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['foo' => 'foo', 'baz' => 'breeze :foo', 'qux' => ['tree :foo', 'breeze :foo', 'beep' => ['rock' => 'tree :foo']]]); + $this->assertEquals(['foo' => 'foo', 'baz' => 'breeze bar', 'qux' => ['tree bar', 'breeze bar', 'beep' => ['rock' => 'tree bar']]], $t->get('foo::bar', ['foo' => 'bar'], 'en')); + $this->assertSame('breeze bar', $t->get('foo::bar.baz', ['foo' => 'bar'], 'en')); + $this->assertSame('foo', $t->get('foo::bar.foo')); + } + + public function testGetMethodForNonExistingReturnsSameKey() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['foo' => 'foo', 'baz' => 'breeze :foo', 'qux' => ['tree :foo', 'breeze :foo']]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'unknown', 'foo')->andReturn([]); + $this->assertSame('foo::unknown', $t->get('foo::unknown', ['foo' => 'bar'], 'en')); + $this->assertSame('foo::bar.unknown', $t->get('foo::bar.unknown', ['foo' => 'bar'], 'en')); + $this->assertSame('foo::unknown.bar', $t->get('foo::unknown.bar')); + } + + public function testTransMethodProperlyLoadsAndRetrievesItemWithHTMLInTheMessage() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo', '*')->andReturn(['bar' => 'breeze

test

']); + $this->assertSame('breeze

test

', $t->get('foo.bar', [], 'en')); + } + + public function testGetMethodProperlyLoadsAndRetrievesItemWithCapitalization() + { + $t = $this->getMockBuilder(Translator::class)->onlyMethods([])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['foo' => 'foo', 'baz' => 'breeze :0 :Foo :BAR']); + $this->assertSame('breeze john Bar FOO', $t->get('foo::bar.baz', ['john', 'foo' => 'bar', 'bar' => 'foo'], 'en')); + $this->assertSame('foo', $t->get('foo::bar.foo')); + } + + public function testGetMethodProperlyLoadsAndRetrievesItemWithLongestReplacementsFirst() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn(['foo' => 'foo', 'baz' => 'breeze :foo :foobar']); + $this->assertSame('breeze bar taylor', $t->get('foo::bar.baz', ['foo' => 'bar', 'foobar' => 'taylor'], 'en')); + $this->assertSame('breeze foo bar baz taylor', $t->get('foo::bar.baz', ['foo' => 'foo bar baz', 'foobar' => 'taylor'], 'en')); + $this->assertSame('foo', $t->get('foo::bar.foo')); + } + + public function testGetMethodProperlyLoadsAndRetrievesItemForFallback() + { + $t = new Translator($this->getLoader(), 'en'); + $t->setFallback('lv'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'bar', 'foo')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('lv', 'bar', 'foo')->andReturn(['foo' => 'foo', 'baz' => 'breeze :foo']); + $this->assertSame('breeze bar', $t->get('foo::bar.baz', ['foo' => 'bar'], 'en')); + $this->assertSame('foo', $t->get('foo::bar.foo')); + } + + public function testGetMethodProperlyLoadsAndRetrievesItemForGlobalNamespace() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo', '*')->andReturn(['bar' => 'breeze :foo']); + $this->assertSame('breeze bar', $t->get('foo.bar', ['foo' => 'bar'])); + } + + public function testSetMethodProperlySetsItem() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->set('foo', 'bar'); + $this->assertSame('bar', $t->get('foo')); + } + + public function testSetMethodOverwritesPreviouslyLoadedItem() + { + $this->assertEquals('Hello Winter!', $this->translator->get('winter.test::lang.test.hello_winter')); + + $this->translator->set('winter.test::lang.test.hello_winter', 'Hi Winter!'); + + $this->assertEquals('Hi Winter!', $this->translator->get('winter.test::lang.test.hello_winter')); + } + + public function testSetMethodDoesNotPreventOtherLanguageStringsBeingLoadedNormally() + { + $path = __DIR__ . '/../fixtures/lang'; + $fileLoader = new FileLoader(new Filesystem(), $path); + $translator = new Translator($fileLoader, 'en'); + + // Set value first + $translator->set('winter.test::lang.test.hello_winter', 'Hi Winter!'); + + // Then, add the namespace + $translator->addNamespace('winter.test', $path); + + $this->assertEquals('Hi Winter!', $translator->get('winter.test::lang.test.hello_winter')); + + // This should now be translated + $this->assertEquals('Welcome to Winter!', $translator->get('winter.test::lang.test.welcome_to_winter')); + } + + public function testSetMethodCanOverwriteAnEntireGroupForALocale() + { + $this->translator->set('winter.test::lang', [ + 'test' => [ + 'hello_winter' => 'Sup Winter?', + 'welcome_to_winter' => 'It\'s time for Winter!', + 'winter' => [ + 'simplicity' => 'Fully simple', + 'stability' => 'Fully stable', + ], + ], + ], 'en_BT'); + + $this->translator->setLocale('en_BT'); + + $this->assertEquals('Sup Winter?', $this->translator->get('winter.test::lang.test.hello_winter')); + $this->assertEquals('It\'s time for Winter!', $this->translator->get('winter.test::lang.test.welcome_to_winter')); + $this->assertEquals('Fully simple', $this->translator->get('winter.test::lang.test.winter.simplicity')); + $this->assertEquals('Fully stable', $this->translator->get('winter.test::lang.test.winter.stability')); + + // Shouldn't be changed + $this->assertEquals('Speed', $this->translator->get('winter.test::lang.test.winter.speed')); + $this->assertEquals('Security', $this->translator->get('winter.test::lang.test.winter.security')); + + $this->translator->setLocale('en'); + + $this->assertEquals('Hello Winter!', $this->translator->get('winter.test::lang.test.hello_winter')); + $this->assertEquals('Welcome to Winter!', $this->translator->get('winter.test::lang.test.welcome_to_winter')); + $this->assertEquals('Simplicity', $this->translator->get('winter.test::lang.test.winter.simplicity')); + $this->assertEquals('Stability', $this->translator->get('winter.test::lang.test.winter.stability')); + + // Shouldn't be changed + $this->assertEquals('Speed', $this->translator->get('winter.test::lang.test.winter.speed')); + $this->assertEquals('Security', $this->translator->get('winter.test::lang.test.winter.security')); + } + + public function testChoiceMethodProperlyLoadsAndRetrievesItem() + { + $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo(['replace']), $this->equalTo('en'))->willReturn('line'); + $t->setSelector($selector = m::mock(MessageSelector::class)); + $selector->shouldReceive('choose')->once()->with('line', 10, 'en')->andReturn('choiced'); + + $t->choice('foo', 10, ['replace']); + } + + public function testChoiceMethodProperlyCountsCollectionsAndLoadsAndRetrievesItem() + { + $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); + $t->expects($this->exactly(2))->method('get')->with($this->equalTo('foo'), $this->equalTo(['replace']), $this->equalTo('en'))->willReturn('line'); + $t->setSelector($selector = m::mock(MessageSelector::class)); + $selector->shouldReceive('choose')->twice()->with('line', 3, 'en')->andReturn('choiced'); + + $values = ['foo', 'bar', 'baz']; + $t->choice('foo', $values, ['replace']); + + $values = new Collection(['foo', 'bar', 'baz']); + $t->choice('foo', $values, ['replace']); + } + + public function testGetJson() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn(['foo' => 'one']); + $this->assertSame('one', $t->get('foo')); + } + + public function testGetJsonReplaces() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn(['foo :i:c :u' => 'bar :i:c :u']); + $this->assertSame('bar onetwo three', $t->get('foo :i:c :u', ['i' => 'one', 'c' => 'two', 'u' => 'three'])); + } + + public function testGetJsonHasAtomicReplacements() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn(['Hello :foo!' => 'Hello :foo!']); + $this->assertSame('Hello baz:bar!', $t->get('Hello :foo!', ['foo' => 'baz:bar', 'bar' => 'abcdef'])); + } + + public function testGetJsonReplacesForAssociativeInput() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn(['foo :i :c' => 'bar :i :c']); + $this->assertSame('bar eye see', $t->get('foo :i :c', ['i' => 'eye', 'c' => 'see'])); + } + + public function testGetJsonPreservesOrder() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn(['to :name I give :greeting' => ':greeting :name']); + $this->assertSame('Greetings David', $t->get('to :name I give :greeting', ['name' => 'David', 'greeting' => 'Greetings'])); + } + + public function testGetJsonForNonExistingJsonKeyLooksForRegularKeys() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo', '*')->andReturn(['bar' => 'one']); + $this->assertSame('one', $t->get('foo.bar')); + } + + public function testGetJsonForNonExistingJsonKeyLooksForRegularKeysAndReplace() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo', '*')->andReturn(['bar' => 'one :message']); + $this->assertSame('one two', $t->get('foo.bar', ['message' => 'two'])); + } + + public function testGetJsonForNonExistingReturnsSameKey() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'Foo that bar', '*')->andReturn([]); + $this->assertSame('Foo that bar', $t->get('Foo that bar')); + } + + public function testGetJsonForNonExistingReturnsSameKeyAndReplaces() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo :message', '*')->andReturn([]); + $this->assertSame('foo baz', $t->get('foo :message', ['message' => 'baz'])); + } + + public function testEmptyFallbacks() + { + $t = new Translator($this->getLoader(), 'en'); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo :message', '*')->andReturn([]); + $this->assertSame('foo ', $t->get('foo :message', ['message' => null])); + } + + public function testDetermineLocalesUsingMethod() + { + $t = new Translator($this->getLoader(), 'en'); + $t->determineLocalesUsing(function ($locales) { + $this->assertSame(['en'], $locales); + + return ['en', 'lz']; + }); + $t->getLoader()->shouldReceive('load')->once()->with('en', '*', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('en', 'foo', '*')->andReturn([]); + $t->getLoader()->shouldReceive('load')->once()->with('lz', 'foo', '*')->andReturn([]); + $this->assertSame('foo', $t->get('foo')); + } + public function testSimilarWordsParsing() { $this->assertEquals( @@ -50,29 +359,26 @@ public function testChoice() */ public function testChoiceSublocale() { - $this->translator->setLocale('en-au'); - $this->assertEquals( - 'Page', - $this->translator->choice('lang.test.choice', 1) + 'mom', + $this->translator->choice('lang.test.mother', 1) ); $this->assertEquals( - 'Pages', - $this->translator->choice('lang.test.choice', 2) + 'moms', + $this->translator->choice('lang.test.mother', 2) ); - } - public function testOverrideWithBeforeResolveEvent() - { - $eventsDispatcher = $this->createMock(Dispatcher::class); - $eventsDispatcher - ->expects($this->exactly(2)) - ->method('fire') - ->will($this->onConsecutiveCalls('Hello Override!', null)); - $this->translator->setEventDispatcher($eventsDispatcher); + $this->translator->setLocale('en-gb'); - $this->assertEquals('Hello Override!', $this->translator->get('lang.test.hello_override')); - $this->assertEquals('Hello Winter!', $this->translator->get('lang.test.hello_winter')); + $this->assertEquals( + 'mum', + $this->translator->choice('lang.test.mother', 1) + ); + + $this->assertEquals( + 'mums', + $this->translator->choice('lang.test.mother', 2) + ); } public function testNamespaceAliasing() diff --git a/tests/fixtures/events/EventTest.php b/tests/fixtures/events/EventTest.php new file mode 100644 index 000000000..91b016959 --- /dev/null +++ b/tests/fixtures/events/EventTest.php @@ -0,0 +1,5 @@ + [ + 'mother' => 'mum|mums', + ], +]; diff --git a/tests/fixtures/lang/en/lang.php b/tests/fixtures/lang/en/lang.php index 9fac1c1d1..f6ab52132 100644 --- a/tests/fixtures/lang/en/lang.php +++ b/tests/fixtures/lang/en/lang.php @@ -4,7 +4,15 @@ 'test' => [ 'pagination' => 'Displayed records: :from-:to of :total', 'hello_winter' => 'Hello Winter!', + 'welcome_to_winter' => 'Welcome to Winter!', + 'winter' => [ + 'simplicity' => 'Simplicity', + 'speed' => 'Speed', + 'stability' => 'Stability', + 'security' => 'Security', + ], 'choice' => 'Page|Pages', + 'mother' => 'mom|moms', ], 'validation' => [ 'fail' => 'Translated fallback message', diff --git a/tests/fixtures/parse/arrayfile/env-config.php b/tests/fixtures/parse/arrayfile/env-config.php new file mode 100644 index 000000000..952e415aa --- /dev/null +++ b/tests/fixtures/parse/arrayfile/env-config.php @@ -0,0 +1,8 @@ + [ + 'value' => env('TEST_ENV', 'default'), + 'no_default' => env('TEST_NO_DEFAULT') + ] +]; diff --git a/tests/fixtures/parse/arrayfile/expression.php b/tests/fixtures/parse/arrayfile/expression.php new file mode 100644 index 000000000..4a9b08d6a --- /dev/null +++ b/tests/fixtures/parse/arrayfile/expression.php @@ -0,0 +1,7 @@ + $bar +]; diff --git a/tests/fixtures/parse/arrayfile/import.php b/tests/fixtures/parse/arrayfile/import.php new file mode 100644 index 000000000..9cc2108f2 --- /dev/null +++ b/tests/fixtures/parse/arrayfile/import.php @@ -0,0 +1,8 @@ + Response::HTTP_OK, + 'bar' => Response::HTTP_I_AM_A_TEAPOT +]; diff --git a/tests/fixtures/parse/arrayfile/include.php b/tests/fixtures/parse/arrayfile/include.php new file mode 100644 index 000000000..267bf9bb2 --- /dev/null +++ b/tests/fixtures/parse/arrayfile/include.php @@ -0,0 +1,13 @@ + array_merge(include(__DIR__ . '/sample-array-file.php'), [ + 'bar' => 'foo' + ]), + 'bar' => 'foo' +]; diff --git a/tests/fixtures/parse/arrayfile/invalid.php b/tests/fixtures/parse/arrayfile/invalid.php new file mode 100644 index 000000000..f734f1b9f --- /dev/null +++ b/tests/fixtures/parse/arrayfile/invalid.php @@ -0,0 +1,10 @@ + winterTest('foo') +]; diff --git a/tests/fixtures/parse/arrayfile/nested-comments.php b/tests/fixtures/parse/arrayfile/nested-comments.php new file mode 100644 index 000000000..846e31a59 --- /dev/null +++ b/tests/fixtures/parse/arrayfile/nested-comments.php @@ -0,0 +1,41 @@ + [ + + /* + |-------------------------------------------------------------------------- + | Enable throttling of Backend authentication attempts + |-------------------------------------------------------------------------- + | + | If set to true, users will be given a limited number of attempts to sign + | in to the Backend before being blocked for a specified number of minutes. + | + */ + + 'enabled' => true, + + /* + |-------------------------------------------------------------------------- + | Failed Authentication Attempt Limit + |-------------------------------------------------------------------------- + | + | Number of failed attempts allowed while trying to authenticate a user. + | + */ + + 'attemptLimit' => 5, + + /* + |-------------------------------------------------------------------------- + | Suspension Time + |-------------------------------------------------------------------------- + | + | The number of minutes to suspend further attempts on authentication once + | the attempt limit is reached. + | + */ + + 'suspensionTime' => 15, + ], +]; diff --git a/tests/fixtures/parse/arrayfile/sample-array-file.php b/tests/fixtures/parse/arrayfile/sample-array-file.php new file mode 100644 index 000000000..9cdb0c712 --- /dev/null +++ b/tests/fixtures/parse/arrayfile/sample-array-file.php @@ -0,0 +1,149 @@ + true, + + // phpcs:ignore + "debugAgain" => FALSE , + + "bullyIan" => 0, + + 'booLeeIan' => 1, + + 'aNumber' => 55, + + 'default' => 'mysql', + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => 'http://localhost', + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. We have gone + | ahead and set this to a sensible default for you out of the box. + | + */ + + 'timezone' => "Winter's time", + + "timezoneAgain" => 'Something "else"' , + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'database' => __DIR__.'/../database/production.sqlite', + 'prefix' => '', + ], + + 'mysql' => [ + 'driver' => ['rabble' => 'mysql'], + 'host' => 'localhost', + 'database' => 'database', + 'username' => 'root', + 'password' => '', + 'charset' => 'utf8', + 'collation' => 'utf8_unicode_ci', + 'prefix' => '', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'host' => 'localhost', + 'database' => 'database', + 'username' => 'root', + 'password' => '', + 'prefix' => '', + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'host' => 'localhost', + 'database' => 'database', + 'username' => 'root', + 'password' => false, + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Memcached Servers + |-------------------------------------------------------------------------- + | + | Now you may specify an array of your Memcached servers that should be + | used when utilizing the Memcached cache driver. All of the servers + | should contain a value for "host", "port", and "weight" options. + | + */ + + 'memcached' => ['host' => '127.0.0.1', 'port' => 11211, 'weight' => true], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer set of commands than a typical key-value systems + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'cluster' => false, + + 'default' => [ + 'host' => '127.0.0.1', + 'password' => null, + 'port' => 6379, + 'database' => 0, + ], + + ], +]; diff --git a/tests/fixtures/parse/arrayfile/single-line-comments-subitem.php b/tests/fixtures/parse/arrayfile/single-line-comments-subitem.php new file mode 100644 index 000000000..948f010a1 --- /dev/null +++ b/tests/fixtures/parse/arrayfile/single-line-comments-subitem.php @@ -0,0 +1,60 @@ + env('BROADCAST_DRIVER', 'null'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + 'pusher' => [ + 'app_id' => env('PUSHER_APP_ID'), + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + 'driver' => 'pusher', + 'key' => env('PUSHER_APP_KEY'), + 'options' => [ + 'cluster' => env('PUSHER_APP_CLUSTER'), + 'useTLS' => true, + ], + 'secret' => env('PUSHER_APP_SECRET'), + ], + 'ably' => [ + 'driver' => 'ably', + 'key' => env('ABLY_KEY'), + ], + 'redis' => [ + 'connection' => 'default', + 'driver' => 'redis', + ], + 'log' => [ + 'driver' => 'log', + ], + 'null' => [ + 'driver' => 'null', + ], + ], +]; diff --git a/tests/fixtures/parse/arrayfile/single-line-comments.php b/tests/fixtures/parse/arrayfile/single-line-comments.php new file mode 100644 index 000000000..1076982d4 --- /dev/null +++ b/tests/fixtures/parse/arrayfile/single-line-comments.php @@ -0,0 +1,35 @@ + [ + + // above property + + 'bool' => true, + 'array' => [ + // empty array comment + ], + 'multi_line' => [ + // empty array comment + // with extra + ], + 'cms' => [ + 'value', + // end of array comment + ], + 'multi_endings' => [ + 'value', + // first line + // last line + ], + 'multi_comment' => [ + 'value', + /* + * Something long + */ + ], + 'callable' => array_merge(config('something'), [ + // configs + ]), + ], +]; diff --git a/tests/fixtures/parse/test.env b/tests/fixtures/parse/test.env new file mode 100644 index 000000000..2af86fdd7 --- /dev/null +++ b/tests/fixtures/parse/test.env @@ -0,0 +1,26 @@ +# WINTERCMS + +APP_DEBUG=true +APP_URL="http://localhost" +APP_KEY="changeme" +# HELLO WORLD + +DB_USE_CONFIG_FOR_TESTING=false +CACHE_DRIVER="file" +SESSION_DRIVER="file" +QUEUE_CONNECTION="sync" + +MAIL_DRIVER="smtp" +MAIL_HOST="smtp.mailgun.org" +MAIL_PORT=587 +MAIL_ENCRYPTION="tls" +MAIL_USERNAME=null +MAIL_PASSWORD=null + +ROUTES_CACHE=false +ASSET_CACHE=false +DATABASE_TEMPLATES=false +LINK_POLICY="detect" +ENABLE_CSRF=true +#ENV_TEST="wintercms" +KEY_WITH_NO_VALUE diff --git a/tests/fixtures/yaml/symfony3.yaml b/tests/fixtures/yaml/symfony3.yaml new file mode 100644 index 000000000..4ad4a0ebd --- /dev/null +++ b/tests/fixtures/yaml/symfony3.yaml @@ -0,0 +1,95 @@ +## +## Numeric keys are not supported: +## +numeric_keys_not_supported: + # Form config file + form: + # field options array, unquoted keys & values + options: + 0.1: 0.1 + 0.2: 0.2 + + # field options array, unquoted keys + options2: + 0.1: '0.1' + 0.2: '0.2' + + # Aligned colons + options3 : + 0.1 : 0.1 + 0.2 : 0.2 + + # version.yaml file + updates: + 1.0.1: First version of Plugin + 1.0.2: + - 'Create plugin tables' + - create_plugin_table.php + 1.1: + - 'Add new component' + - create_component_table.php + 1.1.1: + - 'Update column property' + - update_column_property.php + + + +## +## Could not be parsed as it uses an unsupported built-in tag - FIXED IN VERSIONYAMLPARSER +## +# unsupported_build_tag: +# ## Version.yaml unquoted !!! usage +# 1.0.0: First version of Plugin +# 2.0.0: !!! Updated for Winter v1.2+ +# 3.0.0: +# - Multiple lines of changes +# - !!! Surprise! Some of them are important + + + +# ## +# ## Non-string keys are not supported -> WONTFIX +# ## +# non_string_keys: +# # Reserved types as field options array +# options4: +# null: 'None' +# true: True +# FALSE: 'FALSE' + + + +# ## +# ## Malformed inline YAML string -> WONTFIX +# ## +# malformed_inline_yaml_string: +# ## Colorpicker form widget availableColors option +# ## Documentation has correct example showing values need to be quoted, wontfix +# color: +# label: Custom color +# type: colorpicker +# availableColors: [#000000, #ffffff, #f2f2f2] + + + +# ## +# ## Duplicate Key -> WONTFIX +# ## +# duplicate_key: +# ## Unintentional duplicate keys in form configuration +# myfield: +# label: 'Label' +# comment: author.plugin::lang.fields.myfield +# span: right +# type: text +# comment: 'Untranslated' + + + +# ## +# ## The reserved indicator "@" cannot start a plain scalar -> WONTFIX +# ## +# reserved_indicator: +# ## Old usage of the "@" application path symbol, replaced with ~ in 2015 +# ## @see https://github.com/wintercms/winter/commit/9d649ebb1e72624361f8152f39a8e9c097701792 +# list: @/plugins/myauthor/myplugin/models/mymodel/columns.yaml diff --git a/tests/tmp/.gitignore b/tests/tmp/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/tests/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore