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 72810423a..59d2e7f9d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,6 +22,11 @@ jobs: 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 @@ -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 diff --git a/composer.json b/composer.json index 00ac1f4a8..163a5a278 100644 --- a/composer.json +++ b/composer.json @@ -56,6 +56,7 @@ "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", + "nunomaduro/larastan": "^2.0.1", "orchestra/testbench": "^7.1.0" }, "suggest": { @@ -83,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 @@ 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) { @@ -159,6 +161,7 @@ public function hasUser() /** * Sets the user + * @phpstan-param Models\User $user */ public function setUser(Authenticatable $user) { @@ -244,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.'); @@ -338,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) { @@ -361,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) { @@ -383,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 = []) { @@ -413,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(); } @@ -425,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(); } @@ -621,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) { @@ -656,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) { @@ -714,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) @@ -828,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() { @@ -845,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/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 43985b5ca..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. * diff --git a/src/Console/Command.php b/src/Console/Command.php index c8aaf0166..b3a3c3af8 100644 --- a/src/Console/Command.php +++ b/src/Console/Command.php @@ -97,6 +97,9 @@ public function complete(CompletionInput $input, CompletionSuggestions $suggesti $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) { 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,14 +99,12 @@ 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->getSize(); $this->content_type = $uploadedFile->getMimeType(); @@ -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) { - if ($filePath === null) { - return; - } - $file = new FileObj($filePath); $this->file_name = $file->getFilename(); $this->file_size = $file->getSize(); @@ -141,17 +144,12 @@ public function fromFile($filePath) /** * 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; - } - $tempPath = temp_path($filename); FileHelper::put($tempPath, $data); @@ -163,9 +161,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) { @@ -182,7 +181,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']]; @@ -203,6 +203,7 @@ public function fromUrl($url, $filename = null) /** * Helper attribute for getPath. + * * @return string */ public function getPathAttribute() @@ -212,6 +213,7 @@ public function getPathAttribute() /** * Helper attribute for getExtension. + * * @return string */ public function getExtensionAttribute() @@ -221,6 +223,8 @@ public function getExtensionAttribute() /** * Used only when filling attributes. + * + * @param mixed $value * @return void */ public function setDataAttribute($value) @@ -230,7 +234,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() { @@ -239,11 +246,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() { @@ -252,10 +264,13 @@ public function getHeightAttribute() return $dimensions[1]; } + + return null; } /** * Helper attribute for file size in human format. + * * @return string */ public function getSizeAttribute() @@ -270,33 +285,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], @@ -306,8 +322,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) { @@ -320,17 +337,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(); } // @@ -340,7 +357,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) @@ -354,6 +371,8 @@ public function getCacheKey($path = null) /** * Returns the file name without path + * + * @return string */ public function getFilename() { @@ -362,6 +381,8 @@ public function getFilename() /** * Returns the file extension. + * + * @return string */ public function getExtension() { @@ -370,6 +391,8 @@ public function getExtension() /** * Returns the last modification date as a UNIX timestamp. + * + * @param string|null $fileName * @return int */ public function getLastModified($fileName = null) @@ -379,6 +402,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() { @@ -396,6 +423,9 @@ public function getContentType() /** * Get file contents from storage device. + * + * @param string|null $fileName + * @return string */ public function getContents($fileName = null) { @@ -404,6 +434,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) { @@ -416,6 +449,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() { @@ -436,6 +471,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) @@ -448,15 +485,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; @@ -464,7 +503,8 @@ public function isPublic() /** * Returns the file size as string. - * @return string Returns the size as string. + * + * @return string */ public function sizeToString() { @@ -478,6 +518,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() { @@ -487,8 +529,7 @@ public function beforeSave() if ($this->data !== null) { if ($this->data instanceof UploadedFile) { $this->fromPost($this->data); - } - else { + } else { $this->fromFile($this->data); } @@ -499,14 +540,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) { } } @@ -516,6 +558,8 @@ public function afterDelete() /** * Checks if the file extension is an image and returns true or false. + * + * @return bool */ public function isImage() { @@ -524,7 +568,8 @@ public function isImage() /** * Get image dimensions - * @return array|bool + * + * @return array|false */ protected function getImageDimensions() { @@ -564,8 +609,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); } } @@ -575,16 +619,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 = []) @@ -614,9 +679,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) { @@ -629,18 +702,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); } @@ -651,6 +722,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) { @@ -662,11 +740,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 { @@ -674,8 +751,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); } @@ -690,8 +766,10 @@ protected function makeThumbStorage($thumbFile, $thumbPath, $width, $height, $op FileHelper::delete($tempThumb); } - /* + /** * Delete all thumbnails for this file. + * + * @return void */ public function deleteThumbs() { @@ -712,8 +790,7 @@ public function deleteThumbs() if (!empty($collection)) { if ($this->isLocalStorage()) { FileHelper::delete($collection); - } - else { + } else { $this->getDisk()->delete($collection); } } @@ -725,6 +802,8 @@ public function deleteThumbs() /** * Generates a disk name from the supplied file name. + * + * @return string */ protected function getDiskName() { @@ -739,13 +818,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) { @@ -758,8 +840,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) { @@ -786,10 +870,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); @@ -797,6 +880,8 @@ protected function putFile($sourcePath, $destinationFileName = null) /** * Delete file contents from storage device. + * + * @param string|null $fileName * @return void */ protected function deleteFile($fileName = null) @@ -818,7 +903,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) { @@ -837,8 +924,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) @@ -866,14 +954,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; } @@ -883,10 +969,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() @@ -903,8 +989,7 @@ protected function storageCmd() }, $args); $result = forward_static_call_array([$interface, $command], $args); - } - else { + } else { $result = call_user_func_array([$this->getDisk(), $command], $args); } @@ -913,6 +998,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) { @@ -921,6 +1010,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) { @@ -932,8 +1025,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() { @@ -942,6 +1036,8 @@ public static function getMaxFilesize() /** * Define the internal storage path, override this method to define. + * + * @return string */ public function getStorageDirectory() { @@ -954,6 +1050,8 @@ public function getStorageDirectory() /** * Define the public address for the storage path. + * + * @return string */ public function getPublicPath() { @@ -966,6 +1064,8 @@ public function getPublicPath() /** * Define the internal working path, override this method to define. + * + * @return string */ public function getTempPath() { @@ -980,7 +1080,8 @@ public function getTempPath() /** * Returns the storage disk the file is stored on - * @return FilesystemAdapter + * + * @return Filesystem */ public function getDisk() { @@ -989,6 +1090,7 @@ public function getDisk() /** * Returns true if the storage engine is local. + * * @return bool */ protected function isLocalStorage() @@ -997,12 +1099,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)) . '/'; @@ -1010,10 +1112,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 acc5e1412..ecf581b9c 100644 --- a/src/Database/Concerns/HasRelationships.php +++ b/src/Database/Concerns/HasRelationships.php @@ -151,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; } /** @@ -178,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) { @@ -187,6 +189,8 @@ public function getRelationTypeDefinition($type, $name) if (isset($definitions[$name])) { return $definitions[$name]; } + + return null; } /** @@ -216,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) { @@ -225,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) { @@ -336,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': @@ -465,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) { @@ -487,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) { @@ -508,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) @@ -718,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) { @@ -740,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) { @@ -764,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']; } @@ -808,8 +819,7 @@ protected function addRelation(string $type, string $name, array $config): void sprintf( 'Cannot add the "%s" relation to %s, it conflicts with an existing relation, attribute, or property.', $name, - get_class($this), - $name + get_class($this) ) ); } @@ -946,4 +956,17 @@ 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/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 7d12f0ace..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) { @@ -134,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(); } /* @@ -155,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/Model.php b/src/Database/Model.php index 4e5315fad..e708566f7 100644 --- a/src/Database/Model.php +++ b/src/Database/Model.php @@ -1,13 +1,13 @@ save(null, $sessionKey); + $model->save([], $sessionKey); return $model; } @@ -477,7 +479,7 @@ public static function fetched($callback) /** * Checks if an attribute is jsonable or not. * - * @return array + * @return bool */ public function isJsonable($key) { @@ -530,7 +532,7 @@ public function getObservableEvents() /** * Get a fresh timestamp for the model. * - * @return \Winter\Storm\Argon\Argon + * @return \Illuminate\Support\Carbon */ public function freshTimestamp() { @@ -605,10 +607,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; @@ -621,7 +623,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) { @@ -670,7 +672,7 @@ public function __get($name) public function __set($name, $value) { - return $this->extendableSet($name, $value); + $this->extendableSet($name, $value); } public function __call($name, $params) @@ -741,7 +743,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) { @@ -762,7 +764,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 @@ -803,15 +805,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); @@ -823,10 +816,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); @@ -834,15 +827,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; } @@ -874,11 +868,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); } @@ -1201,7 +1196,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) { @@ -1216,7 +1211,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/QueryBuilder.php b/src/Database/QueryBuilder.php index 8f9ee21b2..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. @@ -388,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; @@ -400,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; @@ -467,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/Traits/Encryptable.php b/src/Database/Traits/Encryptable.php index 7e1844dc4..762eece0d 100644 --- a/src/Database/Traits/Encryptable.php +++ b/src/Database/Traits/Encryptable.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, $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; } @@ -54,13 +53,13 @@ public function packDown($file) $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,12 +67,12 @@ 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; } $instance = require_once $file; @@ -107,7 +106,7 @@ protected function isValidScript($object, $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 5939fda3b..788606829 100644 --- a/src/Events/Dispatcher.php +++ b/src/Events/Dispatcher.php @@ -1,7 +1,6 @@ listen($this->firstClosureParameterType($events), $events, $priority); + $this->listen($this->firstClosureParameterType($events), $events, $priority); + return; } elseif ($events instanceof QueuedClosure) { - return $this->listen($this->firstClosureParameterType($events->closure), $events->resolve(), $priority); + $this->listen($this->firstClosureParameterType($events->closure), $events->resolve(), $priority); + return; } elseif ($listener instanceof QueuedClosure) { $listener = $listener->resolve(); } @@ -102,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) { @@ -193,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) { diff --git a/src/Exception/ErrorHandler.php b/src/Exception/ErrorHandler.php index 84e83c0ed..b429cf99e 100644 --- a/src/Exception/ErrorHandler.php +++ b/src/Exception/ErrorHandler.php @@ -1,10 +1,9 @@ setMask($proposedException); } @@ -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 c48b0c522..157d1ba07 100644 --- a/src/Exception/ExceptionBase.php +++ b/src/Exception/ExceptionBase.php @@ -1,8 +1,8 @@ formatStackArguments($event['args'], false); + $args = $this->formatStackArguments($event['args']); } $result[] = (object)[ diff --git a/src/Extension/ExtendableTrait.php b/src/Extension/ExtendableTrait.php index 8c73be7d0..d8dbf998f 100644 --- a/src/Extension/ExtendableTrait.php +++ b/src/Extension/ExtendableTrait.php @@ -1,12 +1,12 @@ extensionNormalizeClassName($extensionName); @@ -364,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) { @@ -382,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) { diff --git a/src/Filesystem/Definitions.php b/src/Filesystem/Definitions.php index a11a73c07..934bb7f72 100644 --- a/src/Filesystem/Definitions.php +++ b/src/Filesystem/Definitions.php @@ -1,6 +1,6 @@ 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 640d73ca3..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->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/FilesystemManager.php b/src/Filesystem/FilesystemManager.php index 330ac14e2..6a7118a86 100644 --- a/src/Filesystem/FilesystemManager.php +++ b/src/Filesystem/FilesystemManager.php @@ -1,6 +1,5 @@ prefixer->prefixPath(''); }); FilesystemAdapter::macro('setPathPrefix', function (string $prefix) { + /** @phpstan-ignore-next-line */ $this->prefixer = new PathPrefixer($prefix, $this->config['directory_separator'] ?? DIRECTORY_SEPARATOR); }); } 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 @@ instance('path.temp', $this->tempPath()); $this->instance('path.uploads', $this->uploadsPath()); $this->instance('path.media', $this->mediaPath()); + $this->instance('path.lang', $this->langPath()); } /** @@ -224,7 +225,7 @@ public function tempPath() /** * Set the temp path for the application. * - * @return string + * @return static */ public function setTempPath($path) { @@ -247,7 +248,7 @@ public function uploadsPath() /** * Set the uploads path for the application. * - * @return string + * @return static */ public function setUploadsPath($path) { @@ -270,7 +271,7 @@ public function mediaPath() /** * Set the media path for the application. * - * @return string + * @return static */ public function setMediaPath($path) { @@ -311,7 +312,7 @@ public function make($abstract, array $parameters = []) */ public function before($callback) { - return $this['router']->before($callback); + $this['router']->before($callback); } /** @@ -322,7 +323,7 @@ public function before($callback) */ public function after($callback) { - return $this['router']->after($callback); + $this['router']->after($callback); } /** @@ -390,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\\'); }); 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/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 6f66b9515..c9ff12699 100644 --- a/src/Foundation/Bootstrap/RegisterWinter.php +++ b/src/Foundation/Bootstrap/RegisterWinter.php @@ -1,16 +1,16 @@ > */ protected $dontReport = [ \Winter\Storm\Exception\AjaxException::class, @@ -84,7 +84,7 @@ public function report(Throwable $throwable) * * @param \Illuminate\Http\Request $request * @param \Throwable $throwable - * @return \Illuminate\Http\Response + * @return \Symfony\Component\HttpFoundation\Response */ public function render($request, Throwable $throwable) { @@ -157,7 +157,7 @@ public function error(Closure $callback) * * @param \Throwable $throwable * @param bool $fromConsole - * @return void + * @return mixed|null */ protected function callCustomHandlers($throwable, $fromConsole = false) { @@ -176,14 +176,14 @@ protected function callCustomHandlers($throwable, $fromConsole = false) // at least some errors, and avoid errors with no data or not log writes. try { $response = $handler($throwable, $code, $fromConsole); + } catch (Throwable $t) { + $response = $this->convertExceptionToResponse($t); } - catch (Throwable $t) { - $response = $this->convertThrowableToResponse($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; } } @@ -214,11 +214,24 @@ protected function hints(ReflectionFunction $reflection, $throwable) $parameters = $reflection->getParameters(); $expected = $parameters[0]; - try { - return (new ReflectionClass($expected->getType()->getName())) - ->isInstance($throwable); - } 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); } } diff --git a/src/Foundation/Maker.php b/src/Foundation/Maker.php index 648ca1371..9253a16b2 100644 --- a/src/Foundation/Maker.php +++ b/src/Foundation/Maker.php @@ -5,6 +5,8 @@ use Illuminate\Contracts\Container\BindingResolutionException; use ReflectionClass; use ReflectionParameter; +use ReflectionNamedType; +use ReflectionUnionType; class Maker { @@ -29,13 +31,7 @@ public function __construct(Application $app) $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; @@ -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) diff --git a/src/Foundation/Providers/ArtisanServiceProvider.php b/src/Foundation/Providers/ArtisanServiceProvider.php index 7d23dc970..3b6d348b4 100644 --- a/src/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Foundation/Providers/ArtisanServiceProvider.php @@ -141,9 +141,7 @@ public function register() */ protected function registerKeyGenerateCommand() { - $this->app->singleton(KeyGenerateCommand::class, function ($app) { - return new KeyGenerateCommand($app['files']); - }); + $this->app->singleton(KeyGenerateCommand::class); } /** diff --git a/src/Foundation/Providers/ExecutionContextProvider.php b/src/Foundation/Providers/ExecutionContextProvider.php index 2bc2282d4..16178f0f3 100644 --- a/src/Foundation/Providers/ExecutionContextProvider.php +++ b/src/Foundation/Providers/ExecutionContextProvider.php @@ -1,6 +1,7 @@ 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..2753599b1 100644 --- a/src/Halcyon/Datasource/ResolverInterface.php +++ b/src/Halcyon/Datasource/ResolverInterface.php @@ -1,28 +1,48 @@ putInMemoryCache($key, $value); - parent::forever($key, $value); + return parent::forever($key, $value); } /** @@ -127,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) { @@ -139,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 289e20c73..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() { @@ -679,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; } @@ -695,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; } @@ -978,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) @@ -993,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) @@ -1211,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); } @@ -1241,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; @@ -1257,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); @@ -1272,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(); @@ -1305,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; @@ -1402,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() { @@ -1436,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) { @@ -1446,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() { @@ -1477,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() { @@ -1487,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) @@ -1508,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() { @@ -1539,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) { @@ -1551,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(); } /** 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', ''); } @@ -93,7 +94,7 @@ public static function render(array $data, array $options = []): string // Prepare code section for saving $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/Html/BlockBuilder.php b/src/Html/BlockBuilder.php index d072bd25e..575071ea7 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 = ''): 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 = ''): 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 index 7b29ceef9..ade48d425 100644 --- a/src/Mail/MailManager.php +++ b/src/Mail/MailManager.php @@ -36,6 +36,7 @@ public function mailer($name = null) */ protected function resolve($name) { + /** @var array|null */ $config = $this->getConfig($name); if (is_null($config)) { 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 @@ fireEvent('mailer.beforeSend', [$view, $data, $callback], true) === false) || (Event::fire('mailer.beforeSend', [$view, $data, $callback], true) === false) ) { - return; + return null; } if ($view instanceof MailableContract) { @@ -142,7 +142,7 @@ 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; } @@ -195,13 +195,13 @@ public function send($view, array $data = [], $callback = null) * - Support for the Winter MailParser * * @param \Illuminate\Mail\Message $message - * @param string $view - * @param string $plain - * @param string $raw - * @param array $data + * @param string|null $view + * @param string|null $plain + * @param string|null $raw + * @param array|null $data * @return void */ - protected function addContent($message, $view, $plain, $raw, $data) + protected function addContent($message, $view = null, $plain = null, $raw = null, $data = null) { /** * @event mailer.beforeAddContent @@ -286,11 +286,11 @@ protected function addContent($message, $view, $plain, $raw, $data) * Add the raw content to the provided message. * * @param \Illuminate\Mail\Message $message - * @param string $html - * @param string $text + * @param string|null $html + * @param string|null $text * @return void */ - protected function addContentRaw($message, $html, $text) + protected function addContentRaw($message, $html = null, $text = null) { if (isset($html)) { $message->html($html); @@ -304,7 +304,7 @@ protected function addContentRaw($message, $html, $text) /** * 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 @@ -341,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 @@ -401,7 +401,7 @@ protected function buildQueueMailable($view, $data, $callback, $queueName = null /** * Helper for raw() method, send a new message when only a raw text part. * @param array $recipients - * @param string $view + * @param array|string $view * @param mixed $callback * @param array $options * @return \Illuminate\Mail\SentMessage|null @@ -438,10 +438,8 @@ public function sendTo($recipients, $view, array $data = [], $callback = null, $ $queue = $options; $bcc = false; } else { - extract(array_merge([ - 'queue' => false, - 'bcc' => false - ], $options)); + $queue = (bool) ($options['queue'] ?? false); + $bcc = (bool) ($options['bcc'] ?? false); } $method = $queue === true ? 'queue' : 'send'; 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 index ea6a85a08..e0d5bd3ee 100644 --- a/src/Parse/Assetic/Cache/FilesystemCache.php +++ b/src/Parse/Assetic/Cache/FilesystemCache.php @@ -1,8 +1,8 @@ scriptPath = dirname($asset->getSourceRoot() . '/' . $asset->getSourcePath()); @@ -49,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/Parse/Assetic/Filter/ScssCompiler.php b/src/Parse/Assetic/Filter/ScssCompiler.php index fdd563197..59b0d5ae5 100644 --- a/src/Parse/Assetic/Filter/ScssCompiler.php +++ b/src/Parse/Assetic/Filter/ScssCompiler.php @@ -1,11 +1,11 @@ [] ]; - 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/EnvFile.php b/src/Parse/EnvFile.php index 6276885dc..dd590d625 100644 --- a/src/Parse/EnvFile.php +++ b/src/Parse/EnvFile.php @@ -168,7 +168,12 @@ protected function escapeValue($value): string */ protected function parse(string $filePath): array { - if (!file_exists($filePath) || !($contents = file($filePath)) || !count($contents)) { + if (!is_file($filePath)) { + return [[], []]; + } + + $contents = file($filePath); + if (empty($contents)) { return [[], []]; } 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 index fe18024aa..c0b2e7a18 100644 --- a/src/Parse/PHP/ArrayFile.php +++ b/src/Parse/PHP/ArrayFile.php @@ -309,7 +309,7 @@ protected function getAstReturnIndex(array $ast): ?int * If the path cannot be found completely, return the nearest parent and the remainder of the path * * @param array $path - * @param $pointer + * @param mixed $pointer * @param int $depth * @throws SystemException if trying to set a position that is already occupied by a value */ diff --git a/src/Parse/PHP/ArrayPrinter.php b/src/Parse/PHP/ArrayPrinter.php index 04bf38937..81f967daf 100644 --- a/src/Parse/PHP/ArrayPrinter.php +++ b/src/Parse/PHP/ArrayPrinter.php @@ -81,7 +81,7 @@ protected function pMaybeMultiline(array $nodes, bool $trailingComma = false) * * The result includes a leading newline and one level of indentation (same as pStmts). * - * @param Node[] $nodes Array of Nodes to be printed + * @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 diff --git a/src/Parse/Syntax/FieldParser.php b/src/Parse/Syntax/FieldParser.php index 9b389c812..383628575 100644 --- a/src/Parse/Syntax/FieldParser.php +++ b/src/Parse/Syntax/FieldParser.php @@ -59,21 +59,23 @@ class FieldParser ]; /** - * Constructor + * Constructor. + * * @param string $template Template to parse. + * @param array $options */ - public function __construct($template = null, $options = []) + final public function __construct($template, $options = []) { - if ($template) { - $this->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 @@ 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) { + if ($exceptionOnInvalidType === true) { $flags |= YamlComponent::DUMP_EXCEPTION_ON_INVALID_TYPE; } - if ($objectSupport) { + if ($objectSupport === true) { $flags |= YamlComponent::DUMP_OBJECT; } 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..74791ef8a 100644 --- a/src/Router/UrlGenerator.php +++ b/src/Router/UrlGenerator.php @@ -80,14 +80,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 +163,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 +274,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 +325,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/GeneratorCommand.php b/src/Scaffold/GeneratorCommand.php index b7e1c190f..37f2b3136 100644 --- a/src/Scaffold/GeneratorCommand.php +++ b/src/Scaffold/GeneratorCommand.php @@ -1,12 +1,18 @@ files->isDirectory(dirname($path))) { + if (!$this->files->isDirectory(dirname($path))) { $this->files->makeDirectory(dirname($path), 0777, true, true); } } 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 6a4de871a..b3c38ce20 100644 --- a/src/Support/Facades/Schema.php +++ b/src/Support/Facades/Schema.php @@ -45,7 +45,7 @@ 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() { diff --git a/src/Support/ModuleServiceProvider.php b/src/Support/ModuleServiceProvider.php index f7c36a0b8..1a7620e0d 100644 --- a/src/Support/ModuleServiceProvider.php +++ b/src/Support/ModuleServiceProvider.php @@ -51,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 index 45422571f..54587bc71 100644 --- a/src/Support/Serialization.php +++ b/src/Support/Serialization.php @@ -12,11 +12,11 @@ class Serialization * Wraps a closure in a SerializableClosure, returns the provided object if it's not a closure. * * @param Closure|mixed $callable provided callable to be wrapped if it's a closure - * @return mixed|SerializableClosure + * @return SerializableClosure|mixed */ public static function wrapClosure($callable) { - if ($callable instanceof Closure && !($callable instanceof SerializableClosure)) { + if ($callable instanceof Closure) { $callable = new SerializableClosure($callable); } 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 49bbc5044..d52e14b93 100644 --- a/src/Support/Traits/Emitter.php +++ b/src/Support/Traits/Emitter.php @@ -60,7 +60,7 @@ public function bindEvent($event, $callback = null, $priority = 0) /** * Create a new event binding that fires once only * @param string|Closure|QueuedClosure $event - * @param Closure|null $callback When a Closure or QueuedClosure is provided as the first parameter + * @param QueuedClosure|Closure|null $callback When a Closure or QueuedClosure is provided as the first parameter * this parameter can be omitted * @return self */ @@ -81,7 +81,7 @@ public function bindEventOnce($event, $callback = null) * Sort the listeners for a given event by priority. * * @param string $eventName - * @return array + * @return void */ protected function emitterEventSortEvents($eventName) { @@ -96,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) @@ -108,7 +108,7 @@ public function unbindEvent($event = null) foreach ($event as $_event) { $this->unbindEvent($_event); } - return; + return $this; } if (is_object($event)) { @@ -140,7 +140,8 @@ 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) { diff --git a/src/Support/aliases.php b/src/Support/aliases.php index c29f443da..a3debf09e 100644 --- a/src/Support/aliases.php +++ b/src/Support/aliases.php @@ -99,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); /** @@ -141,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); 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 e9b5d1392..c8dd611b6 100644 --- a/src/Support/helpers.php +++ b/src/Support/helpers.php @@ -115,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(); } /* @@ -125,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'; @@ -158,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/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 02a2c9622..aef6454f1 100644 --- a/src/Translation/Translator.php +++ b/src/Translation/Translator.php @@ -16,7 +16,7 @@ class Translator extends TranslatorBase /** * 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; @@ -106,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) { @@ -132,7 +132,7 @@ protected function localeForChoice($locale) { $locale = parent::localeForChoice($locale); - if (!is_null($locale) && str_contains($locale, '-')) { + if (str_contains($locale, '-')) { $localeParts = explode('-', $locale, 2); $locale = $localeParts[0] . '_' . strtoupper($localeParts[1]); } @@ -167,9 +167,13 @@ public function parseKey($key) */ protected function localeArray($locale) { - $locales = array_values(array_filter([$locale ?: $this->locale, $this->fallback, static::CORE_LOCALE])); + $locales = array_values(parent::localeArray($locale)); + + if (!in_array(static::CORE_LOCALE, $locales)) { + $locales[] = static::CORE_LOCALE; + } - return call_user_func($this->determineLocalesUsing ?: fn () => $locales, $locales); + return $locales; } /** 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/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 @@ +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 7c8aa7118..0d2387ce9 100644 --- a/tests/Database/QueryBuilderTest.php +++ b/tests/Database/QueryBuilderTest.php @@ -124,11 +124,13 @@ protected function getConnection($connection = null, $table = null) 'rollBack', 'transactionLevel', 'pretend', - 'getDatabaseName' + 'getDatabaseName', + 'getConfig', ]) ->getMock(); $connection->method('getDatabaseName')->willReturn('database'); + $connection->method('getConfig')->with('use_upsert_alias')->willReturn(false); return $connection; } 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/Html/BlockBuilderTest.php b/tests/Html/BlockBuilderTest.php index 51e82a096..eede6e329 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() 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/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/Translation/TranslatorTest.php b/tests/Translation/TranslatorTest.php index 1ab7b0fc1..29b6f8cd5 100644 --- a/tests/Translation/TranslatorTest.php +++ b/tests/Translation/TranslatorTest.php @@ -5,7 +5,6 @@ use Illuminate\Support\Collection; use Illuminate\Translation\MessageSelector; use Mockery as m; -use Winter\Storm\Events\Dispatcher; use Winter\Storm\Translation\FileLoader; use Winter\Storm\Translation\Translator; @@ -323,7 +322,7 @@ public function testDetermineLocalesUsingMethod() { $t = new Translator($this->getLoader(), 'en'); $t->determineLocalesUsing(function ($locales) { - $this->assertSame(['en', 'en'], $locales); + $this->assertSame(['en'], $locales); return ['en', 'lz']; });