diff --git a/.github/workflows/databases.yml b/.github/workflows/databases.yml index f4457fd85c52..a42954e826bc 100644 --- a/.github/workflows/databases.yml +++ b/.github/workflows/databases.yml @@ -16,7 +16,7 @@ jobs: image: mysql:5.7 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: forge + MYSQL_DATABASE: laravel ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 @@ -51,7 +51,6 @@ jobs: run: vendor/bin/phpunit tests/Integration/Database env: DB_CONNECTION: mysql - DB_USERNAME: root MYSQL_COLLATION: utf8mb4_unicode_ci mysql_8: @@ -62,7 +61,7 @@ jobs: image: mysql:8 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: forge + MYSQL_DATABASE: laravel ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 @@ -97,7 +96,6 @@ jobs: run: vendor/bin/phpunit tests/Integration/Database env: DB_CONNECTION: mysql - DB_USERNAME: root mariadb: runs-on: ubuntu-22.04 @@ -107,7 +105,7 @@ jobs: image: mariadb:10 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: forge + MYSQL_DATABASE: laravel ports: - 3306:3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 @@ -142,7 +140,6 @@ jobs: run: vendor/bin/phpunit tests/Integration/Database env: DB_CONNECTION: mariadb - DB_USERNAME: root pgsql: runs-on: ubuntu-22.04 @@ -151,7 +148,7 @@ jobs: postgresql: image: postgres:14 env: - POSTGRES_DB: forge + POSTGRES_DB: laravel POSTGRES_USER: forge POSTGRES_PASSWORD: password ports: @@ -188,6 +185,7 @@ jobs: run: vendor/bin/phpunit tests/Integration/Database env: DB_CONNECTION: pgsql + DB_USERNAME: forge DB_PASSWORD: password mssql: diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 18b32b3261a9..2aa858fb68e0 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -8,5 +8,5 @@ permissions: pull-requests: write jobs: - uneditable: + pull-requests: uses: laravel/.github/.github/workflows/pull-requests.yml@main diff --git a/.github/workflows/queues.yml b/.github/workflows/queues.yml index dbe5c2bcc1b4..fd583e6c3f8f 100644 --- a/.github/workflows/queues.yml +++ b/.github/workflows/queues.yml @@ -128,10 +128,10 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 - - - uses: actions/checkout@v3 + - name: Download & Extract beanstalkd run: curl -L https://github.com/beanstalkd/beanstalkd/archive/refs/tags/v1.13.tar.gz | tar xz + - name: Make beanstalkd run: make working-directory: beanstalkd-1.13 diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml new file mode 100644 index 000000000000..e6177b2222a8 --- /dev/null +++ b/.github/workflows/releases.yml @@ -0,0 +1,64 @@ +name: manual release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version to release' + required: true + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Remove optional "v" prefix + id: version + run: | + VERSION=${{ inputs.version }} + echo "version=${VERSION#v}" >> "$GITHUB_OUTPUT" + + - name: Update Application.php version + run: sed -i "s/const VERSION = '.*';/const VERSION = '${{ steps.version.outputs.version }}';/g" src/Illuminate/Foundation/Application.php + + - name: Commit version change + uses: stefanzweifel/git-auto-commit-action@v5 + with: + commit_message: "Update version to v${{ steps.version.outputs.version }}" + + - name: SSH into splitter server + uses: appleboy/ssh-action@master + with: + host: 104.248.56.26 + username: forge + key: ${{ secrets.SSH_PRIVATE_KEY_SPLITTER }} + script: | + cd laravel-${{ github.ref_name }} + git pull origin ${{ github.ref_name }} + bash ./bin/release.sh v${{ steps.version.outputs.version }} + script_stop: true + + - name: Generate release notes + id: notes + uses: RedCrafter07/release-notes-action@main + with: + tag-name: v${{ steps.version.outputs.version }} + token: ${{ secrets.GITHUB_TOKEN }} + branch: ${{ github.ref_name }} + + - name: Cleanup release notes + run: | + sed -i '/## What/d' ${{ steps.notes.outputs.release-notes }} + sed -i '/## New Contributors/,$d' ${{ steps.notes.outputs.release-notes }} + + - name: Create release + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ steps.version.outputs.version }} + name: v${{ steps.version.outputs.version }} + body: ${{ steps.notes.outputs.release-notes }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 292e06fd86d3..093ad63b7954 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -40,9 +40,10 @@ jobs: fail-fast: true matrix: php: [8.2, 8.3] + phpunit: ['10.5', '11.0.1'] stability: [prefer-lowest, prefer-stable] - name: PHP ${{ matrix.php }} - ${{ matrix.stability }} + name: PHP ${{ matrix.php }} - PHPUnit ${{ matrix.phpunit }} - ${{ matrix.stability }} steps: - name: Checkout code @@ -62,13 +63,20 @@ jobs: REDIS_CONFIGURE_OPTS: --enable-redis --enable-redis-igbinary --enable-redis-msgpack --enable-redis-lzf --with-liblzf --enable-redis-zstd --with-libzstd --enable-redis-lz4 --with-liblz4 REDIS_LIBS: liblz4-dev, liblzf-dev, libzstd-dev - - name: Set Minimum PHP 8.2 Versions + - name: Set minimum PHP 8.2 versions uses: nick-fields/retry@v2 with: timeout_minutes: 5 max_attempts: 5 command: composer require guzzlehttp/psr7:^2.4 --no-interaction --no-update + - name: Set PHPUnit + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer require phpunit/phpunit:^${{ matrix.phpunit }} --dev --no-interaction --no-update + - name: Install dependencies uses: nick-fields/retry@v2 with: @@ -101,9 +109,10 @@ jobs: fail-fast: true matrix: php: [8.2, 8.3] + phpunit: ['10.5', '11.0.1'] stability: [prefer-lowest, prefer-stable] - name: PHP ${{ matrix.php }} - ${{ matrix.stability }} - Windows + name: PHP ${{ matrix.php }} - PHPUnit ${{ matrix.phpunit }} - ${{ matrix.stability }} - Windows steps: - name: Set git to use LF @@ -131,6 +140,13 @@ jobs: max_attempts: 5 command: composer require guzzlehttp/psr7:~2.4 --no-interaction --no-update + - name: Set PHPUnit + uses: nick-fields/retry@v2 + with: + timeout_minutes: 5 + max_attempts: 5 + command: composer require phpunit/phpunit:~${{ matrix.phpunit }} --dev --no-interaction --no-update + - name: Install dependencies uses: nick-fields/retry@v2 with: diff --git a/.styleci.yml b/.styleci.yml index 44f7cb91093b..2ed89668bb19 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,6 +1,6 @@ php: preset: laravel - version: 8.1 + version: 8.2 finder: not-name: - bad-syntax-strategy.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd05da46d1f..561b779964bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,6 @@ ## [Unreleased](https://github.com/laravel/framework/compare/v11.0.0..master) -## [v11.0.0 (2023-??-??)](https://github.com/laravel/framework/compare/v11.0.0...master) +## [v11.0.0 (2024-??-??)](https://github.com/laravel/framework/compare/v11.0.0...master) Check the upgrade guide in the [Official Laravel Upgrade Documentation](https://laravel.com/docs/11.x/upgrade). Also you can see some release notes in the [Official Laravel Release Documentation](https://laravel.com/docs/11.x/releases). diff --git a/README.md b/README.md index df935e86ac3f..a6fb7790a95a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Laravel has the most extensive and thorough documentation and video tutorial lib You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. -If you're not in the mood to read, [Laracasts](https://laracasts.com) contains over 1100 video tutorials covering a range of topics including Laravel, modern PHP, unit testing, JavaScript, and more. Boost the skill level of yourself and your entire team by digging into our comprehensive video library. +If you're not in the mood to read, [Laracasts](https://laracasts.com) contains thousands of video tutorials covering a range of topics including Laravel, modern PHP, unit testing, JavaScript, and more. Boost the skill level of yourself and your entire team by digging into our comprehensive video library. ## Contributing diff --git a/composer.json b/composer.json index 8b4bd32294e8..9db96509c853 100644 --- a/composer.json +++ b/composer.json @@ -24,18 +24,19 @@ "ext-session": "*", "ext-tokenizer": "*", "composer-runtime-api": "^2.2", - "brick/math": "^0.9.3|^0.10.2|^0.11", + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.3.2", "egulias/email-validator": "^3.2.1|^4.0", "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.12", + "laravel/prompts": "^0.1.15", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", "monolog/monolog": "^3.0", - "nesbot/carbon": "^2.67", + "nesbot/carbon": "^2.72.2|^3.0", "nunomaduro/termwind": "^2.0", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", @@ -90,42 +91,43 @@ "illuminate/testing": "self.version", "illuminate/translation": "self.version", "illuminate/validation": "self.version", - "illuminate/view": "self.version" + "illuminate/view": "self.version", + "spatie/once": "*" }, "require-dev": { "ext-gmp": "*", "ably/ably-php": "^1.0", "aws/aws-sdk-php": "^3.235.5", - "doctrine/dbal": "^4.0", - "fakerphp/faker": "^1.21", - "guzzlehttp/guzzle": "^7.6", + "fakerphp/faker": "^1.23", "league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-ftp": "^3.0", "league/flysystem-path-prefixing": "^3.3", "league/flysystem-read-only": "^3.3", "league/flysystem-sftp-v3": "^3.0", - "mockery/mockery": "^1.5.1", + "mockery/mockery": "^1.6", "nyholm/psr7": "^1.2", "orchestra/testbench-core": "^9.0", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.4.7", - "phpunit/phpunit": "^10.1", + "phpunit/phpunit": "^10.5|^11.0", "predis/predis": "^2.0.2", + "resend/resend-php": "^0.10.0", "symfony/cache": "^7.0", "symfony/http-client": "^7.0", - "symfony/psr-http-message-bridge": "^v7.0.0-BETA1" + "symfony/psr-http-message-bridge": "^7.0" + }, + "conflict": { + "tightenco/collect": "<5.5.33" }, "provide": { "psr/container-implementation": "1.1|2.0", "psr/simple-cache-implementation": "1.0|2.0|3.0" }, - "conflict": { - "tightenco/collect": "<5.5.33" - }, "autoload": { "files": [ "src/Illuminate/Collections/helpers.php", "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", "src/Illuminate/Foundation/helpers.php", "src/Illuminate/Support/helpers.php" ], @@ -163,30 +165,29 @@ "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", - "brianium/paratest": "Required to run tests in parallel (^6.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^4.0).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", - "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.6).", "laravel/tinker": "Required to use the tinker console command (^2.0).", "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).", "league/flysystem-read-only": "Required to use read-only disks (^3.3)", "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", - "mockery/mockery": "Required to use mocking (^1.5.1).", + "mockery/mockery": "Required to use mocking (^1.6).", "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", - "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", - "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8|^10.0.7).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).", "predis/predis": "Required to use the predis connector (^2.0.2).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", - "symfony/cache": "Required to PSR-6 cache bridge (^6.3).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^6.3).", - "symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.3).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.3).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.3).", - "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)." + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.0)." }, "config": { "sort-packages": true, diff --git a/config-stubs/app.php b/config-stubs/app.php new file mode 100644 index 000000000000..f46726731e4a --- /dev/null +++ b/config-stubs/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => env('APP_TIMEZONE', 'UTC'), + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/config/app.php b/config/app.php index cd591805bccd..d4e3180fa6fc 100644 --- a/config/app.php +++ b/config/app.php @@ -12,7 +12,7 @@ | | This value is the name of your application, which will be used when the | framework needs to place the application's name in a notification or - | any other location as required by the application or its packages. + | other UI elements where an application name needs to be displayed. | */ @@ -51,7 +51,7 @@ | | This URL is used by the console to properly generate URLs when using | the Artisan command line tool. You should set this to the root of - | your application so that it is used when running Artisan tasks. + | the application so that it's available within Artisan commands. | */ @@ -80,8 +80,8 @@ |-------------------------------------------------------------------------- | | The application locale determines the default locale that will be used - | by the translation service provider. You are free to set this value - | to any of the locales which will be supported by the application. + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. | */ @@ -119,14 +119,20 @@ |-------------------------------------------------------------------------- | | This key is utilized by Laravel's encryption services and should be set - | to a random, 32 character string or all of the encrypted strings are - | not secure. You should do this prior to deploying the application. + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. | */ + 'cipher' => 'AES-256-CBC', + 'key' => env('APP_KEY'), - 'cipher' => 'AES-256-CBC', + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], /* |-------------------------------------------------------------------------- @@ -143,7 +149,7 @@ 'maintenance' => [ 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), - 'store' => env('APP_MAINTENANCE_STORE', 'redis'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], /* diff --git a/config/auth.php b/config/auth.php index 7bf212ad24a4..0ba5d5d8f10c 100644 --- a/config/auth.php +++ b/config/auth.php @@ -7,15 +7,15 @@ | Authentication Defaults |-------------------------------------------------------------------------- | - | This option controls the default authentication "guard" and password - | reset options for your application. You may change these defaults + | This option defines the default authentication "guard" and password + | reset "broker" for your application. You may change these values | as required, but they're a perfect start for most applications. | */ 'defaults' => [ 'guard' => env('AUTH_GUARD', 'web'), - 'passwords' => 'users', + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), ], /* @@ -27,9 +27,9 @@ | Of course, a great default configuration has been defined for you | which utilizes session storage plus the Eloquent user provider. | - | All authentication drivers have a user provider. This defines how the + | All authentication guards have a user provider, which defines how the | users are actually retrieved out of your database or other storage - | mechanisms used by this application to persist your user's data. + | system used by the application. Typically, Eloquent is utilized. | | Supported: "session" | @@ -47,12 +47,12 @@ | User Providers |-------------------------------------------------------------------------- | - | All authentication drivers have a user provider. This defines how the + | All authentication guards have a user provider, which defines how the | users are actually retrieved out of your database or other storage - | mechanisms used by this application to persist your user's data. + | system used by the application. Typically, Eloquent is utilized. | | If you have multiple user tables or models you may configure multiple - | sources which represent each model / table. These sources may then + | providers to represent the model / table. These providers may then | be assigned to any extra authentication guards you have defined. | | Supported: "database", "eloquent" @@ -76,9 +76,9 @@ | Resetting Passwords |-------------------------------------------------------------------------- | - | You may specify multiple password reset configurations if you have more - | than one user table or model in the application and you want to have - | separate password reset settings based on the specific user types. + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. | | The expiry time is the number of minutes that each reset token will be | considered valid. This security feature keeps tokens short-lived so @@ -93,7 +93,7 @@ 'passwords' => [ 'users' => [ 'provider' => 'users', - 'table' => 'password_reset_tokens', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 'expire' => 60, 'throttle' => 60, ], diff --git a/config/broadcasting.php b/config/broadcasting.php index de5dd4e5ce8c..ebc3fb9cf136 100644 --- a/config/broadcasting.php +++ b/config/broadcasting.php @@ -11,7 +11,7 @@ | framework when an event needs to be broadcast. You may set this to | any of the connections defined in the "connections" array below. | - | Supported: "pusher", "ably", "redis", "log", "null" + | Supported: "reverb", "pusher", "ably", "redis", "log", "null" | */ @@ -23,13 +23,29 @@ |-------------------------------------------------------------------------- | | Here you may define all of the broadcast connections that will be used - | to broadcast events to other systems or over websockets. Samples of + | to broadcast events to other systems or over WebSockets. Samples of | each available type of connection are provided inside this array. | */ 'connections' => [ + 'reverb' => [ + 'driver' => 'reverb', + 'key' => env('REVERB_APP_KEY'), + 'secret' => env('REVERB_APP_SECRET'), + 'app_id' => env('REVERB_APP_ID'), + 'options' => [ + 'host' => env('REVERB_HOST'), + 'port' => env('REVERB_PORT', 443), + 'scheme' => env('REVERB_SCHEME', 'https'), + 'useTLS' => env('REVERB_SCHEME', 'https') === 'https', + ], + 'client_options' => [ + // Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html + ], + ], + 'pusher' => [ 'driver' => 'pusher', 'key' => env('PUSHER_APP_KEY'), @@ -53,11 +69,6 @@ 'key' => env('ABLY_KEY'), ], - 'redis' => [ - 'driver' => 'redis', - 'connection' => env('REDIS_BROADCASTING_CONNECTION', 'default'), - ], - 'log' => [ 'driver' => 'log', ], diff --git a/config/cache.php b/config/cache.php index 4d67e5de8a49..3eb95d106ffa 100644 --- a/config/cache.php +++ b/config/cache.php @@ -15,7 +15,7 @@ | */ - 'default' => env('CACHE_STORE', 'file'), + 'default' => env('CACHE_STORE', 'database'), /* |-------------------------------------------------------------------------- @@ -26,17 +26,13 @@ | well as their drivers. You may even define multiple stores for the | same cache driver to group types of items stored in your caches. | - | Supported drivers: "apc", "array", "database", "file", - | "memcached", "redis", "dynamodb", "octane", "null" + | Supported drivers: "apc", "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", "null" | */ 'stores' => [ - 'apc' => [ - 'driver' => 'apc', - ], - 'array' => [ 'driver' => 'array', 'serialize' => false, @@ -100,8 +96,8 @@ | Cache Key Prefix |-------------------------------------------------------------------------- | - | When utilizing the APC, database, memcached, Redis, or DynamoDB cache - | stores there might be other applications using the same cache. For + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For | that reason, you may prefix every cache key to avoid collisions. | */ diff --git a/config/database.php b/config/database.php index f030206fc946..9dadea5f2896 100644 --- a/config/database.php +++ b/config/database.php @@ -10,21 +10,22 @@ |-------------------------------------------------------------------------- | | Here you may specify which of the database connections below you wish - | to use as your default connection for all database work. Of course - | you may use many connections at once throughout the application. + | to use as your default connection for database operations. This is + | the connection which will be utilized unless another connection + | is explicitly specified when you execute a query / statement. | */ - 'default' => env('DB_CONNECTION', 'mysql'), + 'default' => env('DB_CONNECTION', 'sqlite'), /* |-------------------------------------------------------------------------- | Database Connections |-------------------------------------------------------------------------- | - | Here are each of the database connections setup for your application. - | Of course, examples of configuring each database platform that is - | supported by Laravel is shown below to assist your development. + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. | */ @@ -43,8 +44,8 @@ 'url' => env('DB_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => 'utf8mb4', @@ -59,12 +60,12 @@ ], 'mariadb' => [ - 'driver' => 'mysql', + 'driver' => 'mariadb', 'url' => env('DB_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => 'utf8mb4', @@ -83,8 +84,8 @@ 'url' => env('DB_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '5432'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', 'prefix' => '', @@ -98,8 +99,8 @@ 'url' => env('DB_URL'), 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', '1433'), - 'database' => env('DB_DATABASE', 'forge'), - 'username' => env('DB_USERNAME', 'forge'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), 'password' => env('DB_PASSWORD', ''), 'charset' => 'utf8', 'prefix' => '', @@ -117,7 +118,7 @@ | | This table keeps track of all the migrations that have already run for | your application. Using this information, we can determine which of - | the migrations on disk haven't actually been run in the database. + | the migrations on disk haven't actually been run on the database. | */ @@ -133,7 +134,7 @@ | | Redis is an open source, fast, and advanced key-value store that also | provides a richer body of commands than a typical key-value system - | such as APC or Memcached. Laravel makes it easy to dig right in. + | such as Memcached. You may define your connection settings here. | */ diff --git a/config/filesystems.php b/config/filesystems.php index 21ad5c8bdb40..44fe9c828e0e 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -20,9 +20,9 @@ | Filesystem Disks |-------------------------------------------------------------------------- | - | Here you may configure as many filesystem "disks" as you wish, and you - | may even configure multiple disks of the same driver. Defaults have - | been set up for each driver as an example of the required values. + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. | | Supported Drivers: "local", "ftp", "sftp", "s3" | diff --git a/config/logging.php b/config/logging.php index 06a666ac78c0..d526b64d75a5 100644 --- a/config/logging.php +++ b/config/logging.php @@ -12,9 +12,9 @@ | Default Log Channel |-------------------------------------------------------------------------- | - | This option defines the default log channel that gets used when writing - | messages to the logs. The name specified in this option should match - | one of the channels defined in the "channels" configuration array. + | This option defines the default log channel that is utilized to write + | messages to your logs. The value provided here should match one of + | the channels present in the list of "channels" configured below. | */ @@ -41,17 +41,17 @@ | Log Channels |-------------------------------------------------------------------------- | - | Here you may configure the log channels for your application. Out of - | the box, Laravel uses the Monolog PHP logging library. This gives - | you a variety of powerful log handlers / formatters to utilize. + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. | | Available Drivers: "single", "daily", "slack", "syslog", - | "errorlog", "monolog", - | "custom", "stack" + | "errorlog", "monolog", "custom", "stack" | */ 'channels' => [ + 'stack' => [ 'driver' => 'stack', 'channels' => explode(',', env('LOG_STACK', 'single')), @@ -126,6 +126,7 @@ 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], + ], ]; diff --git a/config/mail.php b/config/mail.php index 07e449a5fa23..a4b9780e9d42 100644 --- a/config/mail.php +++ b/config/mail.php @@ -7,13 +7,14 @@ | Default Mailer |-------------------------------------------------------------------------- | - | This option controls the default mailer that is used to send any email - | messages sent by your application. Alternative mailers may be setup - | and used as needed; however, this mailer will be used by default. + | This option controls the default mailer that is used to send all email + | messages unless another mailer is explicitly specified when sending + | the message. All additional mailers can be configured within the + | "mailers" array. Examples of each type of mailer are provided. | */ - 'default' => env('MAIL_MAILER', 'smtp'), + 'default' => env('MAIL_MAILER', 'log'), /* |-------------------------------------------------------------------------- @@ -24,21 +25,22 @@ | their respective settings. Several examples have been configured for | you and you are free to add your own as your application requires. | - | Laravel supports a variety of mail "transport" drivers to be used while - | delivering an email. You may specify which one you're using for your - | mailers below. You are free to add additional mailers as required. + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. | | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", - | "postmark", "log", "array", "failover" + | "postmark", "log", "array", "failover", "roundrobin" | */ 'mailers' => [ + 'smtp' => [ 'transport' => 'smtp', 'url' => env('MAIL_URL'), - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), - 'port' => env('MAIL_PORT', 587), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), @@ -50,13 +52,6 @@ 'transport' => 'ses', ], - 'mailgun' => [ - 'transport' => 'mailgun', - // 'client' => [ - // 'timeout' => 5, - // ], - ], - 'postmark' => [ 'transport' => 'postmark', // 'message_stream_id' => null, @@ -86,6 +81,7 @@ 'log', ], ], + ], /* diff --git a/config/queue.php b/config/queue.php index 70c9fcf83a4d..4f689e9c7827 100644 --- a/config/queue.php +++ b/config/queue.php @@ -7,9 +7,9 @@ | Default Queue Connection Name |-------------------------------------------------------------------------- | - | Laravel's queue API supports an assortment of back-ends via a single - | API, giving you convenient access to each back-end using the same - | syntax for every one. Here you may define a default connection. + | Laravel's queue supports a variety of backends via a single, unified + | API, giving you convenient access to each backend using identical + | syntax for each. The default queue connection is defined below. | */ @@ -20,9 +20,9 @@ | Queue Connections |-------------------------------------------------------------------------- | - | Here you may configure the connection information for each server that - | is used by your application. A default configuration has been added - | for each back-end shipped with Laravel. You are free to add more. + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. | | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" | @@ -36,6 +36,7 @@ 'database' => [ 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION', null), 'table' => env('DB_QUEUE_TABLE', 'jobs'), 'queue' => env('DB_QUEUE', 'default'), 'retry_after' => env('DB_QUEUE_RETRY_AFTER', 90), @@ -85,7 +86,7 @@ */ 'batching' => [ - 'database' => env('DB_CONNECTION', 'mysql'), + 'database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'job_batches', ], @@ -104,7 +105,7 @@ 'failed' => [ 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), - 'database' => env('DB_CONNECTION', 'mysql'), + 'database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'failed_jobs', ], diff --git a/config/services.php b/config/services.php index a12fbb266b0b..6bb68f6aece2 100644 --- a/config/services.php +++ b/config/services.php @@ -14,13 +14,6 @@ | */ - 'mailgun' => [ - 'domain' => env('MAILGUN_DOMAIN'), - 'secret' => env('MAILGUN_SECRET'), - 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), - 'scheme' => 'https', - ], - 'postmark' => [ 'token' => env('POSTMARK_TOKEN'), ], diff --git a/config/session.php b/config/session.php index 51c77f90af42..d1dc325d4a16 100644 --- a/config/session.php +++ b/config/session.php @@ -9,16 +9,16 @@ | Default Session Driver |-------------------------------------------------------------------------- | - | This option controls the default session "driver" that will be used by - | incoming requests. Laravel supports a variety of storage drivers to - | choose from for session storage. File storage is used by default. + | This option determines the default session driver that is utilized for + | incoming requests. Laravel supports a variety of storage options to + | persist session data. Database storage is a great default choice. | | Supported: "file", "cookie", "database", "apc", | "memcached", "redis", "dynamodb", "array" | */ - 'driver' => env('SESSION_DRIVER', 'file'), + 'driver' => env('SESSION_DRIVER', 'database'), /* |-------------------------------------------------------------------------- @@ -54,9 +54,9 @@ | Session File Location |-------------------------------------------------------------------------- | - | When utilizing the "file" session driver, we need a spot where session - | files may be stored. A default has been set for you but a different - | location may be specified. This is only needed for file sessions. + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. | */ @@ -80,9 +80,9 @@ | Session Database Table |-------------------------------------------------------------------------- | - | When using the "database" session driver, you may specify the table we - | should use to manage the sessions. Of course, a sensible default is - | provided for you; however, you are free to change this as needed. + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. | */ @@ -93,9 +93,9 @@ | Session Cache Store |-------------------------------------------------------------------------- | - | While using one of the framework's cache driven session backends you may - | list a cache store that should be used for these sessions. This value - | must match with one of the application's configured cache "stores". + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. | | Affects: "apc", "dynamodb", "memcached", "redis" | @@ -121,9 +121,9 @@ | Session Cookie Name |-------------------------------------------------------------------------- | - | Here you may change the name of the cookie used to identify a session - | instance by ID. The name specified here will get used every time a - | new session cookie is created by the framework for every driver. + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. | */ @@ -139,7 +139,7 @@ | | The session cookie path determines the path for which the cookie will | be regarded as available. Typically, this will be the root path of - | your application but you are free to change this when necessary. + | your application, but you're free to change this when necessary. | */ @@ -150,9 +150,9 @@ | Session Cookie Domain |-------------------------------------------------------------------------- | - | Here you may change the domain of the cookie used to identify a session - | in your application. This will determine which domains the cookie is - | available to in your application. A sensible default has been set. + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. | */ @@ -178,7 +178,7 @@ | | Setting this value to true will prevent JavaScript from accessing the | value of the cookie and the cookie will only be accessible through - | the HTTP protocol. You are free to modify this option if needed. + | the HTTP protocol. It's unlikely you should disable this option. | */ @@ -193,6 +193,8 @@ | take place, and can be used to mitigate CSRF attacks. By default, we | will set this value to "lax" since this is a secure default value. | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | | Supported: "lax", "strict", "none", null | */ @@ -210,6 +212,6 @@ | */ - 'partitioned' => false, + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), ]; diff --git a/src/Illuminate/Auth/Access/Gate.php b/src/Illuminate/Auth/Access/Gate.php index e8ef93b64271..1f0a72007e9d 100644 --- a/src/Illuminate/Auth/Access/Gate.php +++ b/src/Illuminate/Auth/Access/Gate.php @@ -318,9 +318,9 @@ public function after(callable $callback) } /** - * Determine if the given ability should be granted for the current user. + * Determine if all of the given abilities should be granted for the current user. * - * @param string $ability + * @param iterable|string $ability * @param array|mixed $arguments * @return bool */ @@ -330,9 +330,9 @@ public function allows($ability, $arguments = []) } /** - * Determine if the given ability should be denied for the current user. + * Determine if any of the given abilities should be denied for the current user. * - * @param string $ability + * @param iterable|string $ability * @param array|mixed $arguments * @return bool */ diff --git a/src/Illuminate/Auth/Notifications/ResetPassword.php b/src/Illuminate/Auth/Notifications/ResetPassword.php index 1d8da41bd1a8..efb4573e8be2 100644 --- a/src/Illuminate/Auth/Notifications/ResetPassword.php +++ b/src/Illuminate/Auth/Notifications/ResetPassword.php @@ -25,7 +25,7 @@ class ResetPassword extends Notification /** * The callback that should be used to build the mail message. * - * @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage)|null + * @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage|\Illuminate\Contracts\Mail\Mailable)|null */ public static $toMailCallback; @@ -114,7 +114,7 @@ public static function createUrlUsing($callback) /** * Set a callback that should be used when building the notification mail message. * - * @param \Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage $callback + * @param \Closure(mixed, string): (\Illuminate\Notifications\Messages\MailMessage|\Illuminate\Contracts\Mail\Mailable) $callback * @return void */ public static function toMailUsing($callback) diff --git a/src/Illuminate/Auth/TokenGuard.php b/src/Illuminate/Auth/TokenGuard.php index b1aa7a7e5162..7fe5a9f7802a 100644 --- a/src/Illuminate/Auth/TokenGuard.php +++ b/src/Illuminate/Auth/TokenGuard.php @@ -92,7 +92,7 @@ public function user() /** * Get the token for the current request. * - * @return string + * @return string|null */ public function getTokenForRequest() { diff --git a/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php b/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php index a1e283a32c96..01c673c22f32 100644 --- a/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php +++ b/src/Illuminate/Broadcasting/Broadcasters/AblyBroadcaster.php @@ -232,4 +232,15 @@ public function getAbly() { return $this->ably; } + + /** + * Set the underlying Ably SDK instance. + * + * @param \Ably\AblyRest $ably + * @return void + */ + public function setAbly($ably) + { + $this->ably = $ably; + } } diff --git a/src/Illuminate/Bus/Batchable.php b/src/Illuminate/Bus/Batchable.php index 0b082700f8a2..5cf5706070e9 100644 --- a/src/Illuminate/Bus/Batchable.php +++ b/src/Illuminate/Bus/Batchable.php @@ -35,7 +35,7 @@ public function batch() } if ($this->batchId) { - return Container::getInstance()->make(BatchRepository::class)->find($this->batchId); + return Container::getInstance()->make(BatchRepository::class)?->find($this->batchId); } } @@ -74,7 +74,7 @@ public function withBatchId(string $batchId) * @param int $failedJobs * @param array $failedJobIds * @param array $options - * @param \Carbon\CarbonImmutable $createdAt + * @param \Carbon\CarbonImmutable|null $createdAt * @param \Carbon\CarbonImmutable|null $cancelledAt * @param \Carbon\CarbonImmutable|null $finishedAt * @return array{0: $this, 1: \Illuminate\Support\Testing\Fakes\BatchFake} @@ -86,7 +86,7 @@ public function withFakeBatch(string $id = '', int $failedJobs = 0, array $failedJobIds = [], array $options = [], - CarbonImmutable $createdAt = null, + ?CarbonImmutable $createdAt = null, ?CarbonImmutable $cancelledAt = null, ?CarbonImmutable $finishedAt = null) { diff --git a/src/Illuminate/Bus/DynamoBatchRepository.php b/src/Illuminate/Bus/DynamoBatchRepository.php index 25a4d4a90ebc..7753fa21297c 100644 --- a/src/Illuminate/Bus/DynamoBatchRepository.php +++ b/src/Illuminate/Bus/DynamoBatchRepository.php @@ -102,6 +102,7 @@ public function get($limit = 50, $before = null) ':id' => array_filter(['S' => $before]), ]), 'Limit' => $limit, + 'ScanIndexForward' => false, ]); return array_map( diff --git a/src/Illuminate/Bus/PendingBatch.php b/src/Illuminate/Bus/PendingBatch.php index 60ff3884c8b8..1b3b01bd061f 100644 --- a/src/Illuminate/Bus/PendingBatch.php +++ b/src/Illuminate/Bus/PendingBatch.php @@ -74,6 +74,31 @@ public function add($jobs) return $this; } + /** + * Add a callback to be executed when the batch is stored. + * + * @param callable $callback + * @return $this + */ + public function before($callback) + { + $this->options['before'][] = $callback instanceof Closure + ? new SerializableClosure($callback) + : $callback; + + return $this; + } + + /** + * Get the "before" callbacks that have been registered with the pending batch. + * + * @return array + */ + public function beforeCallbacks() + { + return $this->options['before'] ?? []; + } + /** * Add a callback to be executed after a job in the batch have executed successfully. * @@ -282,7 +307,7 @@ public function dispatch() $repository = $this->container->make(BatchRepository::class); try { - $batch = $repository->store($this); + $batch = $this->store($repository); $batch = $batch->add($this->jobs); } catch (Throwable $e) { @@ -309,7 +334,7 @@ public function dispatchAfterResponse() { $repository = $this->container->make(BatchRepository::class); - $batch = $repository->store($this); + $batch = $this->store($repository); if ($batch) { $this->container->terminating(function () use ($batch) { @@ -344,4 +369,49 @@ protected function dispatchExistingBatch($batch) new BatchDispatched($batch) ); } + + /** + * Dispatch the batch if the given truth test passes. + * + * @param bool|\Closure $boolean + * @return \Illuminate\Bus\Batch|null + */ + public function dispatchIf($boolean) + { + return value($boolean) ? $this->dispatch() : null; + } + + /** + * Dispatch the batch unless the given truth test passes. + * + * @param bool|\Closure $boolean + * @return \Illuminate\Bus\Batch|null + */ + public function dispatchUnless($boolean) + { + return ! value($boolean) ? $this->dispatch() : null; + } + + /** + * Store the batch using the given repository. + * + * @param \Illuminate\Bus\BatchRepository $repository + * @return \Illuminate\Bus\Batch + */ + protected function store($repository) + { + $batch = $repository->store($this); + + collect($this->beforeCallbacks())->each(function ($handler) use ($batch) { + try { + return $handler($batch); + } catch (Throwable $e) { + if (function_exists('report')) { + report($e); + } + } + }); + + return $batch; + } } diff --git a/src/Illuminate/Cache/ArrayLock.php b/src/Illuminate/Cache/ArrayLock.php index 4c20783b2362..8e1ebe203eea 100644 --- a/src/Illuminate/Cache/ArrayLock.php +++ b/src/Illuminate/Cache/ArrayLock.php @@ -87,6 +87,10 @@ public function release() */ protected function getCurrentOwner() { + if (! $this->exists()) { + return null; + } + return $this->store->locks[$this->name]['owner']; } diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index e5c9049997d0..a9dcb2b1fa2b 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -88,6 +88,8 @@ public function resolve($name) throw new InvalidArgumentException("Cache store [{$name}] is not defined."); } + $config = Arr::add($config, 'store', $name); + if (isset($this->customCreators[$config['driver']])) { return $this->callCustomCreator($config); } @@ -122,7 +124,7 @@ protected function createApcDriver(array $config) { $prefix = $this->getPrefix($config); - return $this->repository(new ApcStore(new ApcWrapper, $prefix)); + return $this->repository(new ApcStore(new ApcWrapper, $prefix), $config); } /** @@ -133,7 +135,7 @@ protected function createApcDriver(array $config) */ protected function createArrayDriver(array $config) { - return $this->repository(new ArrayStore($config['serialize'] ?? false)); + return $this->repository(new ArrayStore($config['serialize'] ?? false), $config); } /** @@ -146,7 +148,8 @@ protected function createFileDriver(array $config) { return $this->repository( (new FileStore($this->app['files'], $config['path'], $config['permission'] ?? null)) - ->setLockDirectory($config['lock_path'] ?? null) + ->setLockDirectory($config['lock_path'] ?? null), + $config ); } @@ -167,7 +170,7 @@ protected function createMemcachedDriver(array $config) array_filter($config['sasl'] ?? []) ); - return $this->repository(new MemcachedStore($memcached, $prefix)); + return $this->repository(new MemcachedStore($memcached, $prefix), $config); } /** @@ -177,7 +180,7 @@ protected function createMemcachedDriver(array $config) */ protected function createNullDriver() { - return $this->repository(new NullStore); + return $this->repository(new NullStore, []); } /** @@ -195,7 +198,8 @@ protected function createRedisDriver(array $config) $store = new RedisStore($redis, $this->getPrefix($config), $connection); return $this->repository( - $store->setLockConnection($config['lock_connection'] ?? $connection) + $store->setLockConnection($config['lock_connection'] ?? $connection), + $config ); } @@ -218,9 +222,12 @@ protected function createDatabaseDriver(array $config) $config['lock_timeout'] ?? 86400, ); - return $this->repository($store->setLockConnection( - $this->app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null) - )); + return $this->repository( + $store->setLockConnection( + $this->app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null) + ), + $config + ); } /** @@ -241,7 +248,8 @@ protected function createDynamodbDriver(array $config) $config['attributes']['value'] ?? 'value', $config['attributes']['expiration'] ?? 'expires_at', $this->getPrefix($config) - ) + ), + $config ); } @@ -275,11 +283,12 @@ protected function newDynamodbClient(array $config) * Create a new cache repository with the given implementation. * * @param \Illuminate\Contracts\Cache\Store $store + * @param array $config * @return \Illuminate\Cache\Repository */ - public function repository(Store $store) + public function repository(Store $store, array $config) { - return tap(new Repository($store), function ($repository) { + return tap(new Repository($store, Arr::only($config, ['store'])), function ($repository) { $this->setEventDispatcher($repository); }); } diff --git a/src/Illuminate/Cache/DynamoDbStore.php b/src/Illuminate/Cache/DynamoDbStore.php index 8e2fda7ff55e..31b8dc48ef01 100644 --- a/src/Illuminate/Cache/DynamoDbStore.php +++ b/src/Illuminate/Cache/DynamoDbStore.php @@ -285,7 +285,7 @@ public function add($key, $value, $seconds) ], 'ExpressionAttributeValues' => [ ':now' => [ - 'N' => (string) Carbon::now()->getTimestamp(), + 'N' => (string) $this->currentTime(), ], ], ]); @@ -326,7 +326,7 @@ public function increment($key, $value = 1) ], 'ExpressionAttributeValues' => [ ':now' => [ - 'N' => (string) Carbon::now()->getTimestamp(), + 'N' => (string) $this->currentTime(), ], ':amount' => [ 'N' => (string) $value, @@ -371,7 +371,7 @@ public function decrement($key, $value = 1) ], 'ExpressionAttributeValues' => [ ':now' => [ - 'N' => (string) Carbon::now()->getTimestamp(), + 'N' => (string) $this->currentTime(), ], ':amount' => [ 'N' => (string) $value, @@ -469,7 +469,7 @@ protected function toTimestamp($seconds) { return $seconds > 0 ? $this->availableAt($seconds) - : Carbon::now()->getTimestamp(); + : $this->currentTime(); } /** diff --git a/src/Illuminate/Cache/Events/CacheEvent.php b/src/Illuminate/Cache/Events/CacheEvent.php index 6c9d42c58e59..b6bc49b15c96 100644 --- a/src/Illuminate/Cache/Events/CacheEvent.php +++ b/src/Illuminate/Cache/Events/CacheEvent.php @@ -4,6 +4,13 @@ abstract class CacheEvent { + /** + * The name of the cache store. + * + * @var string|null + */ + public $storeName; + /** * The key of the event. * @@ -21,12 +28,14 @@ abstract class CacheEvent /** * Create a new event instance. * + * @param string|null $storeName * @param string $key * @param array $tags * @return void */ - public function __construct($key, array $tags = []) + public function __construct($storeName, $key, array $tags = []) { + $this->storeName = $storeName; $this->key = $key; $this->tags = $tags; } diff --git a/src/Illuminate/Cache/Events/CacheHit.php b/src/Illuminate/Cache/Events/CacheHit.php index 976c9e4f228b..9802980e3cbe 100644 --- a/src/Illuminate/Cache/Events/CacheHit.php +++ b/src/Illuminate/Cache/Events/CacheHit.php @@ -14,14 +14,15 @@ class CacheHit extends CacheEvent /** * Create a new event instance. * + * @param string|null $storeName * @param string $key * @param mixed $value * @param array $tags * @return void */ - public function __construct($key, $value, array $tags = []) + public function __construct($storeName, $key, $value, array $tags = []) { - parent::__construct($key, $tags); + parent::__construct($storeName, $key, $tags); $this->value = $value; } diff --git a/src/Illuminate/Cache/Events/KeyWritten.php b/src/Illuminate/Cache/Events/KeyWritten.php index 6474dced83b8..49334882cb10 100644 --- a/src/Illuminate/Cache/Events/KeyWritten.php +++ b/src/Illuminate/Cache/Events/KeyWritten.php @@ -21,15 +21,16 @@ class KeyWritten extends CacheEvent /** * Create a new event instance. * + * @param string|null $storeName * @param string $key * @param mixed $value * @param int|null $seconds * @param array $tags * @return void */ - public function __construct($key, $value, $seconds = null, $tags = []) + public function __construct($storeName, $key, $value, $seconds = null, $tags = []) { - parent::__construct($key, $tags); + parent::__construct($storeName, $key, $tags); $this->value = $value; $this->seconds = $seconds; diff --git a/src/Illuminate/Cache/FileStore.php b/src/Illuminate/Cache/FileStore.php index b18e568f6407..9d5e2e95c872 100755 --- a/src/Illuminate/Cache/FileStore.php +++ b/src/Illuminate/Cache/FileStore.php @@ -290,9 +290,11 @@ protected function getPayload($key) // just return null. Otherwise, we'll get the contents of the file and get // the expiration UNIX timestamps from the start of the file's contents. try { - $expire = substr( - $contents = $this->files->get($path, true), 0, 10 - ); + if (is_null($contents = $this->files->get($path, true))) { + return $this->emptyPayload(); + } + + $expire = substr($contents, 0, 10); } catch (Exception) { return $this->emptyPayload(); } diff --git a/src/Illuminate/Cache/RateLimiter.php b/src/Illuminate/Cache/RateLimiter.php index 5f5fac0659b6..afdb9b25a208 100644 --- a/src/Illuminate/Cache/RateLimiter.php +++ b/src/Illuminate/Cache/RateLimiter.php @@ -105,13 +105,26 @@ public function tooManyAttempts($key, $maxAttempts) } /** - * Increment the counter for a given key for a given decay time. + * Increment (by 1) the counter for a given key for a given decay time. * * @param string $key * @param int $decaySeconds * @return int */ public function hit($key, $decaySeconds = 60) + { + return $this->increment($key, $decaySeconds); + } + + /** + * Increment the counter for a given key for a given decay time by a given amount. + * + * @param string $key + * @param int $decaySeconds + * @param int $amount + * @return int + */ + public function increment($key, $decaySeconds = 60, $amount = 1) { $key = $this->cleanRateLimiterKey($key); @@ -121,7 +134,7 @@ public function hit($key, $decaySeconds = 60) $added = $this->cache->add($key, 0, $decaySeconds); - $hits = (int) $this->cache->increment($key); + $hits = (int) $this->cache->increment($key, $amount); if (! $added && $hits == 1) { $this->cache->put($key, 1, $decaySeconds); diff --git a/src/Illuminate/Cache/RedisTagSet.php b/src/Illuminate/Cache/RedisTagSet.php index bf4c53869361..b5fd0e2593bc 100644 --- a/src/Illuminate/Cache/RedisTagSet.php +++ b/src/Illuminate/Cache/RedisTagSet.php @@ -11,13 +11,13 @@ class RedisTagSet extends TagSet * Add a reference entry to the tag set's underlying sorted set. * * @param string $key - * @param int $ttl + * @param int|null $ttl * @param string $updateWhen * @return void */ - public function addEntry(string $key, int $ttl = 0, $updateWhen = null) + public function addEntry(string $key, int $ttl = null, $updateWhen = null) { - $ttl = $ttl > 0 ? Carbon::now()->addSeconds($ttl)->getTimestamp() : -1; + $ttl = is_null($ttl) ? -1 : Carbon::now()->addSeconds($ttl)->getTimestamp(); foreach ($this->tagIds() as $tagKey) { if ($updateWhen) { diff --git a/src/Illuminate/Cache/RedisTaggedCache.php b/src/Illuminate/Cache/RedisTaggedCache.php index b8120be95c03..75c1001ce747 100644 --- a/src/Illuminate/Cache/RedisTaggedCache.php +++ b/src/Illuminate/Cache/RedisTaggedCache.php @@ -14,9 +14,19 @@ class RedisTaggedCache extends TaggedCache */ public function add($key, $value, $ttl = null) { + $seconds = null; + + if ($ttl !== null) { + $seconds = $this->getSeconds($ttl); + + if ($seconds <= 0) { + return false; + } + } + $this->tags->addEntry( $this->itemKey($key), - ! is_null($ttl) ? $this->getSeconds($ttl) : 0 + $seconds ); return parent::add($key, $value, $ttl); @@ -36,9 +46,15 @@ public function put($key, $value, $ttl = null) return $this->forever($key, $value); } + $seconds = $this->getSeconds($ttl); + + if ($seconds <= 0) { + return false; + } + $this->tags->addEntry( $this->itemKey($key), - $this->getSeconds($ttl) + $seconds ); return parent::put($key, $value, $ttl); diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 44f3181f3f4a..98d3cb315b0f 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -48,15 +48,24 @@ class Repository implements ArrayAccess, CacheContract */ protected $default = 3600; + /** + * The cache store configuration options. + * + * @var array + */ + protected $config = []; + /** * Create a new cache repository instance. * * @param \Illuminate\Contracts\Cache\Store $store + * @param array $config * @return void */ - public function __construct(Store $store) + public function __construct(Store $store, array $config = []) { $this->store = $store; + $this->config = $config; } /** @@ -102,11 +111,11 @@ public function get($key, $default = null): mixed // the default value for this cache value. This default could be a callback // so we will execute the value function which will resolve it if needed. if (is_null($value)) { - $this->event(new CacheMissed($key)); + $this->event(new CacheMissed($this->getName(), $key)); $value = value($default); } else { - $this->event(new CacheHit($key, $value)); + $this->event(new CacheHit($this->getName(), $key, $value)); } return $value; @@ -161,7 +170,7 @@ protected function handleManyResult($keys, $key, $value) // the default value for this cache value. This default could be a callback // so we will execute the value function which will resolve it if needed. if (is_null($value)) { - $this->event(new CacheMissed($key)); + $this->event(new CacheMissed($this->getName(), $key)); return (isset($keys[$key]) && ! array_is_list($keys)) ? value($keys[$key]) : null; } @@ -169,7 +178,7 @@ protected function handleManyResult($keys, $key, $value) // If we found a valid value we will fire the "hit" event and return the value // back from this function. The "hit" event gives developers an opportunity // to listen for every possible cache "hit" throughout this applications. - $this->event(new CacheHit($key, $value)); + $this->event(new CacheHit($this->getName(), $key, $value)); return $value; } @@ -217,7 +226,7 @@ public function put($key, $value, $ttl = null) $result = $this->store->put($this->itemKey($key), $value, $seconds); if ($result) { - $this->event(new KeyWritten($key, $value, $seconds)); + $this->event(new KeyWritten($this->getName(), $key, $value, $seconds)); } return $result; @@ -256,7 +265,7 @@ public function putMany(array $values, $ttl = null) if ($result) { foreach ($values as $key => $value) { - $this->event(new KeyWritten($key, $value, $seconds)); + $this->event(new KeyWritten($this->getName(), $key, $value, $seconds)); } } @@ -367,7 +376,7 @@ public function forever($key, $value) $result = $this->store->forever($this->itemKey($key), $value); if ($result) { - $this->event(new KeyWritten($key, $value)); + $this->event(new KeyWritten($this->getName(), $key, $value)); } return $result; @@ -450,7 +459,7 @@ public function forget($key) { return tap($this->store->forget($this->itemKey($key)), function ($result) use ($key) { if ($result) { - $this->event(new KeyForgotten($key)); + $this->event(new KeyForgotten($this->getName(), $key)); } }); } @@ -509,6 +518,8 @@ public function tags($names) $cache = $this->store->tags(is_array($names) ? $names : func_get_args()); + $cache->config = $this->config; + if (! is_null($this->events)) { $cache->setEventDispatcher($this->events); } @@ -544,6 +555,16 @@ protected function getSeconds($ttl) return (int) ($duration > 0 ? $duration : 0); } + /** + * Get the name of the cache store. + * + * @return string|null + */ + protected function getName() + { + return $this->config['store'] ?? null; + } + /** * Determine if the current store supports tags. * diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index c14465c6b3fa..4e8e57267ccf 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -6,6 +6,7 @@ use ArrayAccess; use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; +use Random\Randomizer; class Arr { @@ -225,6 +226,22 @@ public static function last($array, callable $callback = null, $default = null) return static::first(array_reverse($array, true), $callback, $default); } + /** + * Take the first or last {$limit} items from an array. + * + * @param array $array + * @param int $limit + * @return array + */ + public static function take($array, $limit) + { + if ($limit < 0) { + return array_slice($array, $limit, abs($limit)); + } + + return array_slice($array, 0, $limit); + } + /** * Flatten a multi-dimensional array into a single level. * @@ -476,9 +493,7 @@ public static function keyBy($array, $keyBy) */ public static function prependKeysWith($array, $prependWith) { - return Collection::make($array)->mapWithKeys(function ($item, $key) use ($prependWith) { - return [$prependWith.$key => $item]; - })->all(); + return static::mapWithKeys($array, fn ($item, $key) => [$prependWith.$key => $item]); } /** @@ -493,6 +508,32 @@ public static function only($array, $keys) return array_intersect_key($array, array_flip((array) $keys)); } + /** + * Select an array of values from an array. + * + * @param array $array + * @param array|string $keys + * @return array + */ + public static function select($array, $keys) + { + $keys = static::wrap($keys); + + return static::map($array, function ($item) use ($keys) { + $result = []; + + foreach ($keys as $key) { + if (Arr::accessible($item) && Arr::exists($item, $key)) { + $result[$key] = $item[$key]; + } elseif (is_object($item) && isset($item->{$key})) { + $result[$key] = $item->{$key}; + } + } + + return $result; + }); + } + /** * Pluck an array of values from an array. * @@ -663,24 +704,24 @@ public static function random($array, $number = null, $preserveKeys = false) ); } - if (is_null($number)) { - return $array[array_rand($array)]; + if (empty($array) || (! is_null($number) && $number <= 0)) { + return is_null($number) ? null : []; } - if ((int) $number === 0) { - return []; - } + $keys = (new Randomizer)->pickArrayKeys($array, $requested); - $keys = array_rand($array, $number); + if (is_null($number)) { + return $array[$keys[0]]; + } $results = []; if ($preserveKeys) { - foreach ((array) $keys as $key) { + foreach ($keys as $key) { $results[$key] = $array[$key]; } } else { - foreach ((array) $keys as $key) { + foreach ($keys as $key) { $results[] = $array[$key]; } } @@ -732,20 +773,11 @@ public static function set(&$array, $key, $value) * Shuffle the given array and return the result. * * @param array $array - * @param int|null $seed * @return array */ - public static function shuffle($array, $seed = null) + public static function shuffle($array) { - if (is_null($seed)) { - shuffle($array); - } else { - mt_srand($seed); - shuffle($array); - mt_srand(); - } - - return $array; + return (new Randomizer)->shuffleArray($array); } /** diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 7869e1ce65e0..ac0c367c9075 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -918,6 +918,27 @@ public function only($keys) return new static(Arr::only($this->items, $keys)); } + /** + * Select specific values from the items within the collection. + * + * @param \Illuminate\Support\Enumerable|array|string|null $keys + * @return static + */ + public function select($keys) + { + if (is_null($keys)) { + return new static($this->items); + } + + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } + + $keys = is_array($keys) ? $keys : func_get_args(); + + return new static(Arr::select($this->items, $keys)); + } + /** * Get and remove the last N items from the collection. * @@ -1125,12 +1146,11 @@ public function shift($count = 1) /** * Shuffle the items in the collection. * - * @param int|null $seed * @return static */ - public function shuffle($seed = null) + public function shuffle() { - return new static(Arr::shuffle($this->items, $seed)); + return new static(Arr::shuffle($this->items)); } /** diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index 559e2ecbfec7..a92eec560e05 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -889,10 +889,9 @@ public function search($value, $strict = false); /** * Shuffle the items in the collection. * - * @param int|null $seed * @return static */ - public function shuffle($seed = null); + public function shuffle(); /** * Create chunks representing a "sliding window" view of the items in the collection. diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 7d25e9d899d1..288cdbc2c95e 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -953,6 +953,41 @@ public function only($keys) }); } + /** + * Select specific values from the items within the collection. + * + * @param \Illuminate\Support\Enumerable|array|string $keys + * @return static + */ + public function select($keys) + { + if ($keys instanceof Enumerable) { + $keys = $keys->all(); + } elseif (! is_null($keys)) { + $keys = is_array($keys) ? $keys : func_get_args(); + } + + return new static(function () use ($keys) { + if (is_null($keys)) { + yield from $this; + } else { + foreach ($this as $item) { + $result = []; + + foreach ($keys as $key) { + if (Arr::accessible($item) && Arr::exists($item, $key)) { + $result[$key] = $item[$key]; + } elseif (is_object($item) && isset($item->{$key})) { + $result[$key] = $item->{$key}; + } + } + + yield $result; + } + } + }); + } + /** * Push all of the given items onto the collection. * @@ -1058,12 +1093,11 @@ public function search($value, $strict = false) /** * Shuffle the items in the collection. * - * @param int|null $seed * @return static */ - public function shuffle($seed = null) + public function shuffle() { - return $this->passthru('shuffle', func_get_args()); + return $this->passthru('shuffle', []); } /** @@ -1740,6 +1774,8 @@ protected function passthru($method, array $params) */ protected function now() { - return Carbon::now()->timestamp; + return class_exists(Carbon::class) + ? Carbon::now()->timestamp + : time(); } } diff --git a/src/Illuminate/Config/Repository.php b/src/Illuminate/Config/Repository.php index 640d6731bc27..54d33c26aad6 100644 --- a/src/Illuminate/Config/Repository.php +++ b/src/Illuminate/Config/Repository.php @@ -6,6 +6,7 @@ use Illuminate\Contracts\Config\Repository as ConfigContract; use Illuminate\Support\Arr; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; class Repository implements ArrayAccess, ConfigContract { @@ -77,6 +78,106 @@ public function getMany($keys) return $config; } + /** + * Get the specified string configuration value. + * + * @param string $key + * @param mixed $default + * @return string + */ + public function string(string $key, $default = null): string + { + $value = $this->get($key, $default); + + if (! is_string($value)) { + throw new InvalidArgumentException( + sprintf('Configuration value for key [%s] must be a string, %s given.', $key, gettype($value)) + ); + } + + return $value; + } + + /** + * Get the specified integer configuration value. + * + * @param string $key + * @param mixed $default + * @return int + */ + public function integer(string $key, $default = null): int + { + $value = $this->get($key, $default); + + if (! is_int($value)) { + throw new InvalidArgumentException( + sprintf('Configuration value for key [%s] must be an integer, %s given.', $key, gettype($value)) + ); + } + + return $value; + } + + /** + * Get the specified float configuration value. + * + * @param string $key + * @param mixed $default + * @return float + */ + public function float(string $key, $default = null): float + { + $value = $this->get($key, $default); + + if (! is_float($value)) { + throw new InvalidArgumentException( + sprintf('Configuration value for key [%s] must be a float, %s given.', $key, gettype($value)) + ); + } + + return $value; + } + + /** + * Get the specified boolean configuration value. + * + * @param string $key + * @param mixed $default + * @return bool + */ + public function boolean(string $key, $default = null): bool + { + $value = $this->get($key, $default); + + if (! is_bool($value)) { + throw new InvalidArgumentException( + sprintf('Configuration value for key [%s] must be a boolean, %s given.', $key, gettype($value)) + ); + } + + return $value; + } + + /** + * Get the specified array configuration value. + * + * @param string $key + * @param mixed $default + * @return array + */ + public function array(string $key, $default = null): array + { + $value = $this->get($key, $default); + + if (! is_array($value)) { + throw new InvalidArgumentException( + sprintf('Configuration value for key [%s] must be an array, %s given.', $key, gettype($value)) + ); + } + + return $value; + } + /** * Set a given configuration value. * diff --git a/src/Illuminate/Console/Concerns/ConfiguresPrompts.php b/src/Illuminate/Console/Concerns/ConfiguresPrompts.php index 7bca27f45376..8ebd3faf7207 100644 --- a/src/Illuminate/Console/Concerns/ConfiguresPrompts.php +++ b/src/Illuminate/Console/Concerns/ConfiguresPrompts.php @@ -2,6 +2,7 @@ namespace Illuminate\Console\Concerns; +use Illuminate\Console\PromptValidationException; use Laravel\Prompts\ConfirmPrompt; use Laravel\Prompts\MultiSearchPrompt; use Laravel\Prompts\MultiSelectPrompt; @@ -11,6 +12,7 @@ use Laravel\Prompts\SelectPrompt; use Laravel\Prompts\SuggestPrompt; use Laravel\Prompts\TextPrompt; +use stdClass; use Symfony\Component\Console\Input\InputInterface; trait ConfiguresPrompts @@ -27,6 +29,8 @@ protected function configurePrompts(InputInterface $input) Prompt::interactive(($input->isInteractive() && defined('STDIN') && stream_isatty(STDIN)) || $this->laravel->runningUnitTests()); + Prompt::validateUsing(fn (Prompt $prompt) => $this->validatePrompt($prompt->value(), $prompt->validate)); + Prompt::fallbackWhen(windows_os() || $this->laravel->runningUnitTests()); TextPrompt::fallbackUsing(fn (TextPrompt $prompt) => $this->promptUntilValid( @@ -132,15 +136,21 @@ protected function promptUntilValid($prompt, $required, $validate) if ($required && ($result === '' || $result === [] || $result === false)) { $this->components->error(is_string($required) ? $required : 'Required.'); - continue; + if ($this->laravel->runningUnitTests()) { + throw new PromptValidationException; + } else { + continue; + } } - if ($validate) { - $error = $validate($result); + $error = is_callable($validate) ? $validate($result) : $this->validatePrompt($result, $validate); - if (is_string($error) && strlen($error) > 0) { - $this->components->error($error); + if (is_string($error) && strlen($error) > 0) { + $this->components->error($error); + if ($this->laravel->runningUnitTests()) { + throw new PromptValidationException; + } else { continue; } } @@ -149,6 +159,76 @@ protected function promptUntilValid($prompt, $required, $validate) } } + /** + * Validate the given prompt value using the validator. + * + * @param mixed $value + * @param mixed $rules + * @return ?string + */ + protected function validatePrompt($value, $rules) + { + if ($rules instanceof stdClass) { + $messages = $rules->messages ?? []; + $attributes = $rules->attributes ?? []; + $rules = $rules->rules ?? null; + } + + if (! $rules) { + return; + } + + $field = 'answer'; + + if (is_array($rules) && ! array_is_list($rules)) { + [$field, $rules] = [key($rules), current($rules)]; + } + + return $this->getPromptValidatorInstance( + $field, $value, $rules, $messages ?? [], $attributes ?? [] + )->errors()->first(); + } + + /** + * Get the validator instance that should be used to validate prompts. + * + * @param string $value + * @param mixed $value + * @param mixed $rules + * @param array $messages + * @param array $attributes + * @return \Illuminate\Validation\Validator + */ + protected function getPromptValidatorInstance($field, $value, $rules, array $messages = [], array $attributes = []) + { + return $this->laravel['validator']->make( + [$field => $value], + [$field => $rules], + empty($messages) ? $this->validationMessages() : $messages, + empty($attributes) ? $this->validationAttributes() : $attributes, + ); + } + + /** + * Get the validation messages that should be used during prompt validation. + * + * @return array + */ + protected function validationMessages() + { + return []; + } + + /** + * Get the validation attributes that should be used during prompt validation. + * + * @return array + */ + protected function validationAttributes() + { + return []; + } + /** * Restore the prompts output. * diff --git a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php index 4182a007ed3a..b36a8c5ca566 100644 --- a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php +++ b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php @@ -14,7 +14,7 @@ trait CreatesMatchingTest */ protected function addTestOptions() { - foreach (['test' => 'PHPUnit', 'pest' => 'Pest'] as $option => $name) { + foreach (['test' => 'Test', 'pest' => 'Pest', 'phpunit' => 'PHPUnit'] as $option => $name) { $this->getDefinition()->addOption(new InputOption( $option, null, @@ -32,13 +32,14 @@ protected function addTestOptions() */ protected function handleTestCreation($path) { - if (! $this->option('test') && ! $this->option('pest')) { + if (! $this->option('test') && ! $this->option('pest') && ! $this->option('phpunit')) { return false; } return $this->callSilent('make:test', [ 'name' => Str::of($path)->after($this->laravel['path'])->beforeLast('.php')->append('Test')->replace('\\', '/'), '--pest' => $this->option('pest'), + '--phpunit' => $this->option('phpunit'), ]) == 0; } } diff --git a/src/Illuminate/Console/Concerns/InteractsWithSignals.php b/src/Illuminate/Console/Concerns/InteractsWithSignals.php index 895072c15c72..c93b98dc4e6d 100644 --- a/src/Illuminate/Console/Concerns/InteractsWithSignals.php +++ b/src/Illuminate/Console/Concerns/InteractsWithSignals.php @@ -17,7 +17,9 @@ trait InteractsWithSignals /** * Define a callback to be run when the given signal(s) occurs. * - * @param iterable|int $signals + * @template TSignals of iterable|int + * + * @param (\Closure():(TSignals))|TSignals $signals * @param callable(int $signal): void $callback * @return void */ @@ -28,7 +30,7 @@ public function trap($signals, $callback) $this->getApplication()->getSignalRegistry(), ); - collect(Arr::wrap($signals)) + collect(Arr::wrap(value($signals))) ->each(fn ($signal) => $this->signals->register($signal, $callback)); }); } diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index f061dc67d384..1dc5ed792ee2 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -84,6 +84,7 @@ abstract class GeneratorCommand extends Command implements PromptsForMissingInpu 'namespace', 'new', 'or', + 'parent', 'print', 'private', 'protected', @@ -249,7 +250,7 @@ protected function possibleModels() { $modelPath = is_dir(app_path('Models')) ? app_path('Models') : app_path(); - return collect((new Finder)->files()->depth(0)->in($modelPath)) + return collect(Finder::create()->files()->depth(0)->in($modelPath)) ->map(fn ($file) => $file->getBasename('.php')) ->sort() ->values() @@ -269,7 +270,7 @@ protected function possibleEvents() return []; } - return collect((new Finder)->files()->depth(0)->in($eventPath)) + return collect(Finder::create()->files()->depth(0)->in($eventPath)) ->map(fn ($file) => $file->getBasename('.php')) ->sort() ->values() diff --git a/src/Illuminate/Console/MigrationGeneratorCommand.php b/src/Illuminate/Console/MigrationGeneratorCommand.php index fe1d98d81aa8..c741c03358fe 100644 --- a/src/Illuminate/Console/MigrationGeneratorCommand.php +++ b/src/Illuminate/Console/MigrationGeneratorCommand.php @@ -4,6 +4,8 @@ use Illuminate\Filesystem\Filesystem; +use function Illuminate\Filesystem\join_paths; + abstract class MigrationGeneratorCommand extends Command { /** @@ -102,7 +104,7 @@ protected function replaceMigrationPlaceholders($path, $table) protected function migrationExists($table) { return count($this->files->glob( - $this->laravel->joinPaths($this->laravel->databasePath('migrations'), '*_*_*_*_create_'.$table.'_table.php') + join_paths($this->laravel->databasePath('migrations'), '*_*_*_*_create_'.$table.'_table.php') )) !== 0; } } diff --git a/src/Illuminate/Console/PromptValidationException.php b/src/Illuminate/Console/PromptValidationException.php new file mode 100644 index 000000000000..218720967a0b --- /dev/null +++ b/src/Illuminate/Console/PromptValidationException.php @@ -0,0 +1,9 @@ +validateArray($key, $value) - : CookieValuePrefix::validate($key, $value, $this->encrypter->getKey()); + : CookieValuePrefix::validate($key, $value, $this->encrypter->getAllKeys()); } /** @@ -240,4 +240,16 @@ public static function serialized($name) { return static::$serialize; } + + /** + * Flush the middleware's global state. + * + * @return void + */ + public static function flushState() + { + static::$neverEncrypt = []; + + static::$serialize = false; + } } diff --git a/src/Illuminate/Database/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index 99670cf0949c..df60c61b6d87 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -119,6 +119,10 @@ protected function handleTransactionException(Throwable $e, $currentAttempt, $ma */ public function beginTransaction() { + foreach ($this->beforeStartingTransaction as $callback) { + $callback($this); + } + $this->createTransaction(); $this->transactions++; diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index c046b00a3fad..9aa5fb61a861 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -5,8 +5,6 @@ use Carbon\CarbonInterval; use Closure; use DateTimeInterface; -use Doctrine\DBAL\Connection as DoctrineConnection; -use Doctrine\DBAL\Types\Type; use Exception; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Events\QueryExecuted; @@ -190,25 +188,18 @@ class Connection implements ConnectionInterface protected $pretending = false; /** - * All of the callbacks that should be invoked before a query is executed. + * All of the callbacks that should be invoked before a transaction is started. * * @var \Closure[] */ - protected $beforeExecutingCallbacks = []; - - /** - * The instance of Doctrine connection. - * - * @var \Doctrine\DBAL\Connection - */ - protected $doctrineConnection; + protected $beforeStartingTransaction = []; /** - * Type mappings that should be registered with new Doctrine connections. + * All of the callbacks that should be invoked before a query is executed. * - * @var array + * @var \Closure[] */ - protected $doctrineTypeMappings = []; + protected $beforeExecutingCallbacks = []; /** * The connection resolvers. @@ -1024,8 +1015,6 @@ protected function tryAgainIfCausedByLostConnection(QueryException $e, $query, $ public function reconnect() { if (is_callable($this->reconnector)) { - $this->doctrineConnection = null; - return call_user_func($this->reconnector, $this); } @@ -1052,8 +1041,19 @@ public function reconnectIfMissingConnection() public function disconnect() { $this->setPdo(null)->setReadPdo(null); + } + + /** + * Register a hook to be run just before a database transaction is started. + * + * @param \Closure $callback + * @return $this + */ + public function beforeStartingTransaction(Closure $callback) + { + $this->beforeStartingTransaction[] = $callback; - $this->doctrineConnection = null; + return $this; } /** @@ -1243,106 +1243,6 @@ public function useWriteConnectionWhenReading($value = true) return $this; } - /** - * Is Doctrine available? - * - * @return bool - */ - public function isDoctrineAvailable() - { - return class_exists('Doctrine\DBAL\Connection'); - } - - /** - * Indicates whether native alter operations will be used when dropping, renaming, or modifying columns, even if Doctrine DBAL is installed. - * - * @return bool - */ - public function usingNativeSchemaOperations() - { - return ! $this->isDoctrineAvailable() || SchemaBuilder::$alwaysUsesNativeSchemaOperationsIfPossible; - } - - /** - * Get a Doctrine Schema Column instance. - * - * @param string $table - * @param string $column - * @return \Doctrine\DBAL\Schema\Column - */ - public function getDoctrineColumn($table, $column) - { - $schema = $this->getDoctrineSchemaManager(); - - return $schema->introspectTable($table)->getColumn($column); - } - - /** - * Get the Doctrine DBAL schema manager for the connection. - * - * @return \Doctrine\DBAL\Schema\AbstractSchemaManager - */ - public function getDoctrineSchemaManager() - { - $connection = $this->getDoctrineConnection(); - - return $connection->createSchemaManager(); - } - - /** - * Get the Doctrine DBAL database connection instance. - * - * @return \Doctrine\DBAL\Connection - */ - public function getDoctrineConnection() - { - if (is_null($this->doctrineConnection)) { - $driver = $this->getDoctrineDriver(); - - $this->doctrineConnection = new DoctrineConnection(array_filter([ - 'pdo' => $this->getPdo(), - 'dbname' => $this->getDatabaseName(), - 'driver' => $driver->getName(), - 'serverVersion' => $this->getConfig('server_version'), - ]), $driver); - - foreach ($this->doctrineTypeMappings as $name => $type) { - $this->doctrineConnection - ->getDatabasePlatform() - ->registerDoctrineTypeMapping($type, $name); - } - } - - return $this->doctrineConnection; - } - - /** - * Register a custom Doctrine mapping type. - * - * @param Type|class-string $class - * @param string $name - * @param string $type - * @return void - * - * @throws \Doctrine\DBAL\Exception - * @throws \RuntimeException - */ - public function registerDoctrineType(Type|string $class, string $name, string $type): void - { - if (! $this->isDoctrineAvailable()) { - throw new RuntimeException( - 'Registering a custom Doctrine type requires Doctrine DBAL (doctrine/dbal).' - ); - } - - if (! Type::hasType($name)) { - Type::getTypeRegistry() - ->register($name, is_string($class) ? new $class() : $class); - } - - $this->doctrineTypeMappings[$name] = $type; - } - /** * Get the current PDO connection. * @@ -1757,6 +1657,16 @@ public function withTablePrefix(Grammar $grammar) return $grammar; } + /** + * Get the server version for the connection. + * + * @return string + */ + public function getServerVersion(): string + { + return $this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); + } + /** * Register a connection resolver. * diff --git a/src/Illuminate/Database/Connectors/ConnectionFactory.php b/src/Illuminate/Database/Connectors/ConnectionFactory.php index 80b25d0223a6..f63123652747 100755 --- a/src/Illuminate/Database/Connectors/ConnectionFactory.php +++ b/src/Illuminate/Database/Connectors/ConnectionFactory.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Container\Container; use Illuminate\Database\Connection; +use Illuminate\Database\MariaDbConnection; use Illuminate\Database\MySqlConnection; use Illuminate\Database\PostgresConnection; use Illuminate\Database\SQLiteConnection; @@ -241,6 +242,7 @@ public function createConnector(array $config) return match ($config['driver']) { 'mysql' => new MySqlConnector, + 'mariadb' => new MariaDbConnector, 'pgsql' => new PostgresConnector, 'sqlite' => new SQLiteConnector, 'sqlsrv' => new SqlServerConnector, @@ -268,6 +270,7 @@ protected function createConnection($driver, $connection, $database, $prefix = ' return match ($driver) { 'mysql' => new MySqlConnection($connection, $database, $prefix, $config), + 'mariadb' => new MariaDbConnection($connection, $database, $prefix, $config), 'pgsql' => new PostgresConnection($connection, $database, $prefix, $config), 'sqlite' => new SQLiteConnection($connection, $database, $prefix, $config), 'sqlsrv' => new SqlServerConnection($connection, $database, $prefix, $config), diff --git a/src/Illuminate/Database/Connectors/MariaDbConnector.php b/src/Illuminate/Database/Connectors/MariaDbConnector.php new file mode 100755 index 000000000000..560e31c96916 --- /dev/null +++ b/src/Illuminate/Database/Connectors/MariaDbConnector.php @@ -0,0 +1,32 @@ +exec("use `{$config['database']}`;"); } - $this->configureIsolationLevel($connection, $config); - - $this->configureEncoding($connection, $config); - - // Next, we will check to see if a timezone has been specified in this config - // and if it has we will issue a statement to modify the timezone with the - // database. Setting this DB timezone is an optional configuration item. - $this->configureTimezone($connection, $config); - - $this->setModes($connection, $config); + $this->configureConnection($connection, $config); return $connection; } - /** - * Set the connection transaction isolation level. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureIsolationLevel($connection, array $config) - { - if (! isset($config['isolation_level'])) { - return; - } - - $connection->prepare( - "SET SESSION TRANSACTION ISOLATION LEVEL {$config['isolation_level']}" - )->execute(); - } - - /** - * Set the connection character set and collation. - * - * @param \PDO $connection - * @param array $config - * @return void|\PDO - */ - protected function configureEncoding($connection, array $config) - { - if (! isset($config['charset'])) { - return $connection; - } - - $connection->prepare( - "set names '{$config['charset']}'".$this->getCollation($config) - )->execute(); - } - - /** - * Get the collation for the configuration. - * - * @param array $config - * @return string - */ - protected function getCollation(array $config) - { - return isset($config['collation']) ? " collate '{$config['collation']}'" : ''; - } - - /** - * Set the timezone on the connection. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function configureTimezone($connection, array $config) - { - if (isset($config['timezone'])) { - $connection->prepare('set time_zone="'.$config['timezone'].'"')->execute(); - } - } - /** * Create a DSN string from a configuration. * @@ -155,54 +85,70 @@ protected function getHostDsn(array $config) } /** - * Set the modes for the connection. + * Configure the given PDO connection. * * @param \PDO $connection * @param array $config * @return void */ - protected function setModes(PDO $connection, array $config) + protected function configureConnection(PDO $connection, array $config) { - if (isset($config['modes'])) { - $this->setCustomModes($connection, $config); - } elseif (isset($config['strict'])) { - if ($config['strict']) { - $connection->prepare($this->strictMode($connection, $config))->execute(); + $statements = []; + + if (isset($config['isolation_level'])) { + $statements[] = sprintf('SESSION TRANSACTION ISOLATION LEVEL %s', $config['isolation_level']); + } + + if (isset($config['charset'])) { + if (isset($config['collation'])) { + $statements[] = sprintf("NAMES '%s' COLLATE '%s'", $config['charset'], $config['collation']); } else { - $connection->prepare("set session sql_mode='NO_ENGINE_SUBSTITUTION'")->execute(); + $statements[] = sprintf("NAMES '%s'", $config['charset']); } } - } - /** - * Set the custom modes on the connection. - * - * @param \PDO $connection - * @param array $config - * @return void - */ - protected function setCustomModes(PDO $connection, array $config) - { - $modes = implode(',', $config['modes']); + if (isset($config['timezone'])) { + $statements[] = sprintf("time_zone='%s'", $config['timezone']); + } - $connection->prepare("set session sql_mode='{$modes}'")->execute(); + $sqlMode = $this->getSqlMode($connection, $config); + + if ($sqlMode !== null) { + $statements[] = sprintf("SESSION sql_mode='%s'", $sqlMode); + } + + if ($statements !== []) { + $connection->exec(sprintf('SET %s;', implode(', ', $statements))); + } } /** - * Get the query to enable strict mode. + * Get the sql_mode value. * * @param \PDO $connection * @param array $config - * @return string + * @return string|null */ - protected function strictMode(PDO $connection, $config) + protected function getSqlMode(PDO $connection, array $config) { + if (isset($config['modes'])) { + return implode(',', $config['modes']); + } + + if (! isset($config['strict'])) { + return null; + } + + if (! $config['strict']) { + return 'NO_ENGINE_SUBSTITUTION'; + } + $version = $config['version'] ?? $connection->getAttribute(PDO::ATTR_SERVER_VERSION); if (version_compare($version, '8.0.11') >= 0) { - return "set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'"; + return 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'; } - return "set session sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'"; + return 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'; } } diff --git a/src/Illuminate/Database/Console/DatabaseInspectionCommand.php b/src/Illuminate/Database/Console/DatabaseInspectionCommand.php index 42568fc2c02d..f5ba9036e6ef 100644 --- a/src/Illuminate/Database/Console/DatabaseInspectionCommand.php +++ b/src/Illuminate/Database/Console/DatabaseInspectionCommand.php @@ -2,169 +2,37 @@ namespace Illuminate\Database\Console; -use Doctrine\DBAL\Platforms\AbstractPlatform; use Illuminate\Console\Command; use Illuminate\Database\ConnectionInterface; +use Illuminate\Database\MariaDbConnection; use Illuminate\Database\MySqlConnection; use Illuminate\Database\PostgresConnection; -use Illuminate\Database\QueryException; use Illuminate\Database\SQLiteConnection; use Illuminate\Database\SqlServerConnection; use Illuminate\Support\Arr; -use Illuminate\Support\Composer; -use Symfony\Component\Process\Exception\ProcessSignaledException; -use Symfony\Component\Process\Exception\RuntimeException; -use Symfony\Component\Process\Process; - -use function Laravel\Prompts\confirm; abstract class DatabaseInspectionCommand extends Command { /** - * A map of database column types. - * - * @var array - */ - protected $typeMappings = [ - 'bit' => 'string', - 'citext' => 'string', - 'enum' => 'string', - 'geometry' => 'string', - 'geomcollection' => 'string', - 'linestring' => 'string', - 'ltree' => 'string', - 'multilinestring' => 'string', - 'multipoint' => 'string', - 'multipolygon' => 'string', - 'point' => 'string', - 'polygon' => 'string', - 'sysname' => 'string', - ]; - - /** - * The Composer instance. - * - * @var \Illuminate\Support\Composer - */ - protected $composer; - - /** - * Create a new command instance. - * - * @param \Illuminate\Support\Composer|null $composer - * @return void - */ - public function __construct(Composer $composer = null) - { - parent::__construct(); - - $this->composer = $composer ?? $this->laravel->make(Composer::class); - } - - /** - * Register the custom Doctrine type mappings for inspection commands. - * - * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform - * @return void - */ - protected function registerTypeMappings(AbstractPlatform $platform) - { - foreach ($this->typeMappings as $type => $value) { - $platform->registerDoctrineTypeMapping($type, $value); - } - } - - /** - * Get a human-readable platform name for the given platform. + * Get a human-readable name for the given connection. * - * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform + * @param \Illuminate\Database\ConnectionInterface $connection * @param string $database * @return string */ - protected function getPlatformName(AbstractPlatform $platform, $database) - { - return match (class_basename($platform)) { - 'MySQLPlatform' => 'MySQL <= 5', - 'MySQL57Platform' => 'MySQL 5.7', - 'MySQL80Platform' => 'MySQL 8', - 'PostgreSQL100Platform', 'PostgreSQLPlatform' => 'Postgres', - 'SqlitePlatform' => 'SQLite', - 'SQLServerPlatform' => 'SQL Server', - 'SQLServer2012Platform' => 'SQL Server 2012', - default => $database, - }; - } - - /** - * Get the size of a table in bytes. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param string $table - * @return int|null - */ - protected function getTableSize(ConnectionInterface $connection, string $table) + protected function getConnectionName(ConnectionInterface $connection, $database) { return match (true) { - $connection instanceof MySqlConnection => $this->getMySQLTableSize($connection, $table), - $connection instanceof PostgresConnection => $this->getPostgresTableSize($connection, $table), - $connection instanceof SQLiteConnection => $this->getSqliteTableSize($connection, $table), - default => null, + $connection instanceof MySqlConnection && $connection->isMaria() => 'MariaDB', + $connection instanceof MySqlConnection => 'MySQL', + $connection instanceof MariaDbConnection => 'MariaDB', + $connection instanceof PostgresConnection => 'PostgreSQL', + $connection instanceof SQLiteConnection => 'SQLite', + $connection instanceof SqlServerConnection => 'SQL Server', + default => $database, }; } - /** - * Get the size of a MySQL table in bytes. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param string $table - * @return mixed - */ - protected function getMySQLTableSize(ConnectionInterface $connection, string $table) - { - $result = $connection->selectOne('SELECT (data_length + index_length) AS size FROM information_schema.TABLES WHERE table_schema = ? AND table_name = ?', [ - $connection->getDatabaseName(), - $table, - ]); - - return Arr::wrap((array) $result)['size']; - } - - /** - * Get the size of a Postgres table in bytes. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param string $table - * @return mixed - */ - protected function getPostgresTableSize(ConnectionInterface $connection, string $table) - { - $result = $connection->selectOne('SELECT pg_total_relation_size(?) AS size;', [ - $table, - ]); - - return Arr::wrap((array) $result)['size']; - } - - /** - * Get the size of a SQLite table in bytes. - * - * @param \Illuminate\Database\ConnectionInterface $connection - * @param string $table - * @return mixed - */ - protected function getSqliteTableSize(ConnectionInterface $connection, string $table) - { - try { - $result = $connection->selectOne('SELECT SUM(pgsize) AS size FROM dbstat WHERE name=?', [ - $table, - ]); - - return Arr::wrap((array) $result)['size']; - } catch (QueryException) { - return null; - } - } - /** * Get the number of open connections for a database. * @@ -175,8 +43,8 @@ protected function getConnectionCount(ConnectionInterface $connection) { $result = match (true) { $connection instanceof MySqlConnection => $connection->selectOne('show status where variable_name = "threads_connected"'), - $connection instanceof PostgresConnection => $connection->selectOne('select count(*) AS "Value" from pg_stat_activity'), - $connection instanceof SqlServerConnection => $connection->selectOne('SELECT COUNT(*) Value FROM sys.dm_exec_sessions WHERE status = ?', ['running']), + $connection instanceof PostgresConnection => $connection->selectOne('select count(*) as "Value" from pg_stat_activity'), + $connection instanceof SqlServerConnection => $connection->selectOne('select count(*) Value from sys.dm_exec_sessions where status = ?', ['running']), default => null, }; @@ -201,48 +69,18 @@ protected function getConfigFromDatabase($database) } /** - * Ensure the dependencies for the database commands are available. - * - * @return bool - */ - protected function ensureDependenciesExist() - { - return tap(interface_exists('Doctrine\DBAL\Driver'), function ($dependenciesExist) { - if (! $dependenciesExist && confirm('Inspecting database information requires the Doctrine DBAL (doctrine/dbal) package. Would you like to install it?', default: false)) { - $this->installDependencies(); - } - }); - } - - /** - * Install the command's dependencies. - * - * @return void + * Remove the table prefix from a table name, if it exists. * - * @throws \Symfony\Component\Process\Exception\ProcessSignaledException + * @param \Illuminate\Database\ConnectionInterface $connection + * @param string $table + * @return string */ - protected function installDependencies() + protected function withoutTablePrefix(ConnectionInterface $connection, string $table) { - $command = collect($this->composer->findComposer()) - ->push('require doctrine/dbal:^3.5.1') - ->implode(' '); - - $process = Process::fromShellCommandline($command, null, null, null, null); + $prefix = $connection->getTablePrefix(); - if ('\\' !== DIRECTORY_SEPARATOR && file_exists('/dev/tty') && is_readable('/dev/tty')) { - try { - $process->setTty(true); - } catch (RuntimeException $e) { - $this->components->warn($e->getMessage()); - } - } - - try { - $process->run(fn ($type, $line) => $this->output->write($line)); - } catch (ProcessSignaledException $e) { - if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) { - throw $e; - } - } + return str_starts_with($table, $prefix) + ? substr($table, strlen($prefix)) + : $table; } } diff --git a/src/Illuminate/Database/Console/DbCommand.php b/src/Illuminate/Database/Console/DbCommand.php index caecafe3a644..dce6bc32dd17 100644 --- a/src/Illuminate/Database/Console/DbCommand.php +++ b/src/Illuminate/Database/Console/DbCommand.php @@ -131,6 +131,7 @@ public function getCommand(array $connection) { return [ 'mysql' => 'mysql', + 'mariadb' => 'mysql', 'pgsql' => 'psql', 'sqlite' => 'sqlite3', 'sqlsrv' => 'sqlcmd', @@ -156,6 +157,17 @@ protected function getMysqlArguments(array $connection) ], $connection), [$connection['database']]); } + /** + * Get the arguments for the MariaDB CLI. + * + * @param array $connection + * @return array + */ + protected function getMariaDbArguments(array $connection) + { + return $this->getMysqlArguments($connection); + } + /** * Get the arguments for the Postgres CLI. * @@ -192,6 +204,7 @@ protected function getSqlsrvArguments(array $connection) 'password' => ['-P', $connection['password']], 'host' => ['-S', 'tcp:'.$connection['host'] .($connection['port'] ? ','.$connection['port'] : ''), ], + 'trust_server_certificate' => ['-C'], ], $connection)); } diff --git a/src/Illuminate/Database/Console/Migrations/MigrateCommand.php b/src/Illuminate/Database/Console/Migrations/MigrateCommand.php index d7a8c9e35a11..5f7e1c87b10f 100755 --- a/src/Illuminate/Database/Console/Migrations/MigrateCommand.php +++ b/src/Illuminate/Database/Console/Migrations/MigrateCommand.php @@ -148,7 +148,7 @@ protected function repositoryExists() if ( $e->getPrevious() instanceof PDOException && $e->getPrevious()->getCode() === 1049 && - $connection->getDriverName() === 'mysql') { + in_array($connection->getDriverName(), ['mysql', 'mariadb'])) { return $this->createMissingMysqlDatabase($connection); } @@ -175,9 +175,9 @@ protected function createMissingSqliteDatabase($path) return false; } - $this->components->warn('The SQLite database does not exist: '.$path); + $this->components->warn('The SQLite database configured for this application does not exist: '.$path); - if (! confirm('Would you like to create it?', default: false)) { + if (! confirm('Would you like to create it?', default: true)) { return false; } @@ -202,7 +202,7 @@ protected function createMissingMysqlDatabase($connection) if (! $this->option('force') && ! $this->option('no-interaction')) { $this->components->warn("The database '{$connection->getDatabaseName()}' does not exist on the '{$connection->getName()}' connection."); - if (! confirm('Would you like to create it?', default: false)) { + if (! confirm('Would you like to create it?', default: true)) { return false; } } diff --git a/src/Illuminate/Database/Console/MonitorCommand.php b/src/Illuminate/Database/Console/MonitorCommand.php index 3dff3158268c..d87a441c015c 100644 --- a/src/Illuminate/Database/Console/MonitorCommand.php +++ b/src/Illuminate/Database/Console/MonitorCommand.php @@ -5,7 +5,6 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Events\DatabaseBusy; -use Illuminate\Support\Composer; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'db:monitor')] @@ -46,11 +45,10 @@ class MonitorCommand extends DatabaseInspectionCommand * * @param \Illuminate\Database\ConnectionResolverInterface $connection * @param \Illuminate\Contracts\Events\Dispatcher $events - * @param \Illuminate\Support\Composer $composer */ - public function __construct(ConnectionResolverInterface $connection, Dispatcher $events, Composer $composer) + public function __construct(ConnectionResolverInterface $connection, Dispatcher $events) { - parent::__construct($composer); + parent::__construct(); $this->connection = $connection; $this->events = $events; diff --git a/src/Illuminate/Database/Console/PruneCommand.php b/src/Illuminate/Database/Console/PruneCommand.php index 0c51beb345b2..23875d1187b9 100644 --- a/src/Illuminate/Database/Console/PruneCommand.php +++ b/src/Illuminate/Database/Console/PruneCommand.php @@ -24,6 +24,7 @@ class PruneCommand extends Command protected $signature = 'model:prune {--model=* : Class names of the models to be pruned} {--except=* : Class names of the models to be excluded from pruning} + {--path=* : Absolute path(s) to directories where models are located} {--chunk=1000 : The number of models to retrieve per chunk of models to be deleted} {--pretend : Display the number of prunable records found instead of deleting them}'; @@ -125,7 +126,7 @@ protected function models() throw new InvalidArgumentException('The --models and --except options cannot be combined.'); } - return collect((new Finder)->in($this->getDefaultPath())->files()->name('*.php')) + return collect(Finder::create()->in($this->getPath())->files()->name('*.php')) ->map(function ($model) { $namespace = $this->laravel->getNamespace(); @@ -146,12 +147,18 @@ protected function models() } /** - * Get the default path where models are located. + * Get the path where models are located. * - * @return string|string[] + * @return string[]|string */ - protected function getDefaultPath() + protected function getPath() { + if (! empty($path = $this->option('path'))) { + return collect($path)->map(function ($path) { + return base_path($path); + })->all(); + } + return app_path('Models'); } diff --git a/src/Illuminate/Database/Console/ShowCommand.php b/src/Illuminate/Database/Console/ShowCommand.php index c125c38820e4..d711a01bc685 100644 --- a/src/Illuminate/Database/Console/ShowCommand.php +++ b/src/Illuminate/Database/Console/ShowCommand.php @@ -2,12 +2,11 @@ namespace Illuminate\Database\Console; -use Doctrine\DBAL\Schema\AbstractSchemaManager; -use Doctrine\DBAL\Schema\Table; -use Doctrine\DBAL\Schema\View; use Illuminate\Database\ConnectionInterface; use Illuminate\Database\ConnectionResolverInterface; +use Illuminate\Database\Schema\Builder; use Illuminate\Support\Arr; +use Illuminate\Support\Number; use Symfony\Component\Console\Attribute\AsCommand; #[AsCommand(name: 'db:show')] @@ -20,8 +19,9 @@ class ShowCommand extends DatabaseInspectionCommand */ protected $signature = 'db:show {--database= : The database connection} {--json : Output the database information as JSON} - {--counts : Show the table row count Note: This can be slow on large databases }; - {--views : Show the database views Note: This can be slow on large databases }'; + {--counts : Show the table row count Note: This can be slow on large databases } + {--views : Show the database views Note: This can be slow on large databases } + {--types : Show the user defined types}'; /** * The console command description. @@ -38,28 +38,26 @@ class ShowCommand extends DatabaseInspectionCommand */ public function handle(ConnectionResolverInterface $connections) { - if (! $this->ensureDependenciesExist()) { - return 1; - } - $connection = $connections->connection($database = $this->input->getOption('database')); - $doctrineConnection = $connection->getDoctrineConnection(); - $schema = $connection->getDoctrineSchemaManager(); - - $this->registerTypeMappings($doctrineConnection->getDatabasePlatform()); + $schema = $connection->getSchemaBuilder(); $data = [ 'platform' => [ 'config' => $this->getConfigFromDatabase($database), - 'name' => $this->getPlatformName($doctrineConnection->getDatabasePlatform(), $database), + 'name' => $this->getConnectionName($connection, $database), + 'version' => $connection->getServerVersion(), 'open_connections' => $this->getConnectionCount($connection), ], 'tables' => $this->tables($connection, $schema), ]; if ($this->option('views')) { - $data['views'] = $this->collectViews($connection, $schema); + $data['views'] = $this->views($connection, $schema); + } + + if ($this->option('types')) { + $data['types'] = $this->types($connection, $schema); } $this->display($data); @@ -71,17 +69,19 @@ public function handle(ConnectionResolverInterface $connections) * Get information regarding the tables within the database. * * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @param \Illuminate\Database\Schema\Builder $schema * @return \Illuminate\Support\Collection */ - protected function tables(ConnectionInterface $connection, AbstractSchemaManager $schema) + protected function tables(ConnectionInterface $connection, Builder $schema) { - return collect($schema->listTables())->map(fn (Table $table, $index) => [ - 'table' => $table->getName(), - 'size' => $this->getTableSize($connection, $table->getName()), + return collect($schema->getTables())->map(fn ($table) => [ + 'table' => $table['name'], + 'schema' => $table['schema'], + 'size' => $table['size'], 'rows' => $this->option('counts') ? $connection->table($table->getName())->count() : null, - 'engine' => rescue(fn () => $table->getOption('engine'), null, false), - 'comment' => $table->getComment(), + 'engine' => $table['engine'], + 'collation' => $table['collation'], + 'comment' => $table['comment'], ]); } @@ -89,20 +89,38 @@ protected function tables(ConnectionInterface $connection, AbstractSchemaManager * Get information regarding the views within the database. * * @param \Illuminate\Database\ConnectionInterface $connection - * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema + * @param \Illuminate\Database\Schema\Builder $schema * @return \Illuminate\Support\Collection */ - protected function collectViews(ConnectionInterface $connection, AbstractSchemaManager $schema) + protected function views(ConnectionInterface $connection, Builder $schema) { - return collect($schema->listViews()) - ->reject(fn (View $view) => str($view->getName()) - ->startsWith(['pg_catalog', 'information_schema', 'spt_'])) - ->map(fn (View $view) => [ - 'view' => $view->getName(), + return collect($schema->getViews()) + ->reject(fn ($view) => str($view['name'])->startsWith(['pg_catalog', 'information_schema', 'spt_'])) + ->map(fn ($view) => [ + 'view' => $view['name'], + 'schema' => $view['schema'], 'rows' => $connection->table($view->getName())->count(), ]); } + /** + * Get information regarding the user-defined types within the database. + * + * @param \Illuminate\Database\ConnectionInterface $connection + * @param \Illuminate\Database\Schema\Builder $schema + * @return \Illuminate\Support\Collection + */ + protected function types(ConnectionInterface $connection, Builder $schema) + { + return collect($schema->getTypes()) + ->map(fn ($type) => [ + 'name' => $type['name'], + 'schema' => $type['schema'], + 'type' => $type['type'], + 'category' => $type['category'], + ]); + } + /** * Render the database information. * @@ -136,10 +154,11 @@ protected function displayForCli(array $data) $platform = $data['platform']; $tables = $data['tables']; $views = $data['views'] ?? null; + $types = $data['types'] ?? null; $this->newLine(); - $this->components->twoColumnDetail(''.$platform['name'].''); + $this->components->twoColumnDetail(''.$platform['name'].'', $platform['version']); $this->components->twoColumnDetail('Database', Arr::get($platform['config'], 'database')); $this->components->twoColumnDetail('Host', Arr::get($platform['config'], 'host')); $this->components->twoColumnDetail('Port', Arr::get($platform['config'], 'port')); @@ -149,22 +168,27 @@ protected function displayForCli(array $data) $this->components->twoColumnDetail('Tables', $tables->count()); if ($tableSizeSum = $tables->sum('size')) { - $this->components->twoColumnDetail('Total Size', number_format($tableSizeSum / 1024 / 1024, 2).'MiB'); + $this->components->twoColumnDetail('Total Size', Number::fileSize($tableSizeSum, 2)); } $this->newLine(); if ($tables->isNotEmpty()) { - $this->components->twoColumnDetail('Table', 'Size (MiB)'.($this->option('counts') ? ' / Rows' : '')); + $hasSchema = ! is_null($tables->first()['schema']); + + $this->components->twoColumnDetail( + ($hasSchema ? 'Schema / ' : '').'Table', + 'Size'.($this->option('counts') ? ' / Rows' : '') + ); $tables->each(function ($table) { if ($tableSize = $table['size']) { - $tableSize = number_format($tableSize / 1024 / 1024, 2); + $tableSize = Number::fileSize($tableSize, 2); } $this->components->twoColumnDetail( - $table['table'].($this->output->isVerbose() ? ' '.$table['engine'].'' : null), - ($tableSize ? $tableSize : '—').($this->option('counts') ? ' / '.number_format($table['rows']).'' : '') + ($table['schema'] ? $table['schema'].' / ' : '').$table['table'].($this->output->isVerbose() ? ' '.$table['engine'].'' : null), + ($tableSize ?: '—').($this->option('counts') ? ' / '.Number::format($table['rows']).'' : '') ); if ($this->output->isVerbose()) { @@ -180,9 +204,33 @@ protected function displayForCli(array $data) } if ($views && $views->isNotEmpty()) { - $this->components->twoColumnDetail('View', 'Rows'); + $hasSchema = ! is_null($views->first()['schema']); + + $this->components->twoColumnDetail( + ($hasSchema ? 'Schema / ' : '').'View', + 'Rows' + ); + + $views->each(fn ($view) => $this->components->twoColumnDetail( + ($view['schema'] ? $view['schema'].' / ' : '').$view['view'], + Number::format($view['rows']) + )); + + $this->newLine(); + } + + if ($types && $types->isNotEmpty()) { + $hasSchema = ! is_null($types->first()['schema']); + + $this->components->twoColumnDetail( + ($hasSchema ? 'Schema / ' : '').'Type', + 'Type / Category' + ); - $views->each(fn ($view) => $this->components->twoColumnDetail($view['view'], number_format($view['rows']))); + $types->each(fn ($type) => $this->components->twoColumnDetail( + ($type['schema'] ? $type['schema'].' / ' : '').$type['name'], + $type['type'].' / '.$type['category'] + )); $this->newLine(); } diff --git a/src/Illuminate/Database/Console/ShowModelCommand.php b/src/Illuminate/Database/Console/ShowModelCommand.php index 3ef912004e8c..4ab262546c15 100644 --- a/src/Illuminate/Database/Console/ShowModelCommand.php +++ b/src/Illuminate/Database/Console/ShowModelCommand.php @@ -3,9 +3,6 @@ namespace Illuminate\Database\Console; use BackedEnum; -use Doctrine\DBAL\Schema\Column; -use Doctrine\DBAL\Schema\Index; -use Doctrine\DBAL\Types\DecimalType; use Illuminate\Contracts\Container\BindingResolutionException; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; @@ -70,10 +67,6 @@ class ShowModelCommand extends DatabaseInspectionCommand */ public function handle() { - if (! $this->ensureDependenciesExist()) { - return 1; - } - $class = $this->qualifyModel($this->argument('model')); try { @@ -81,7 +74,9 @@ public function handle() $class = get_class($model); } catch (BindingResolutionException $e) { - return $this->components->error($e->getMessage()); + $this->components->error($e->getMessage()); + + return 1; } if ($this->option('database')) { @@ -97,6 +92,8 @@ public function handle() $this->getRelations($model), $this->getObservers($model), ); + + return 0; } /** @@ -121,25 +118,23 @@ protected function getPolicy($model) protected function getAttributes($model) { $connection = $model->getConnection(); - $schema = $connection->getDoctrineSchemaManager(); - $this->registerTypeMappings($connection->getDoctrineConnection()->getDatabasePlatform()); - $table = $model->getConnection()->getTablePrefix().$model->getTable(); - $columns = $schema->listTableColumns($table); - $indexes = $schema->listTableIndexes($table); + $schema = $connection->getSchemaBuilder(); + $table = $model->getTable(); + $columns = $schema->getColumns($table); + $indexes = $schema->getIndexes($table); return collect($columns) - ->values() - ->map(fn (Column $column) => [ - 'name' => $column->getName(), - 'type' => $this->getColumnType($column), - 'increments' => $column->getAutoincrement(), - 'nullable' => ! $column->getNotnull(), + ->map(fn ($column) => [ + 'name' => $column['name'], + 'type' => $column['type'], + 'increments' => $column['auto_increment'], + 'nullable' => $column['nullable'], 'default' => $this->getColumnDefault($column, $model), - 'unique' => $this->columnIsUnique($column->getName(), $indexes), - 'fillable' => $model->isFillable($column->getName()), - 'hidden' => $this->attributeIsHidden($column->getName(), $model), + 'unique' => $this->columnIsUnique($column['name'], $indexes), + 'fillable' => $model->isFillable($column['name']), + 'hidden' => $this->attributeIsHidden($column['name'], $model), 'appended' => null, - 'cast' => $this->getCastType($column->getName(), $model), + 'cast' => $this->getCastType($column['name'], $model), ]) ->merge($this->getVirtualAttributes($model, $columns)); } @@ -148,7 +143,7 @@ protected function getAttributes($model) * Get the virtual (non-column) attributes for the given model. * * @param \Illuminate\Database\Eloquent\Model $model - * @param \Doctrine\DBAL\Schema\Column[] $columns + * @param array $columns * @return \Illuminate\Support\Collection */ protected function getVirtualAttributes($model, $columns) @@ -170,7 +165,7 @@ protected function getVirtualAttributes($model, $columns) return []; } }) - ->reject(fn ($cast, $name) => collect($columns)->has($name)) + ->reject(fn ($cast, $name) => collect($columns)->contains('name', $name)) ->map(fn ($cast, $name) => [ 'name' => $name, 'type' => null, @@ -428,45 +423,21 @@ protected function getCastsWithDates($model) ->merge($model->getCasts()); } - /** - * Get the type of the given column. - * - * @param \Doctrine\DBAL\Schema\Column $column - * @return string - */ - protected function getColumnType($column) - { - $name = $column->getType()->getName(); - - $unsigned = $column->getUnsigned() ? ' unsigned' : ''; - - $details = match (get_class($column->getType())) { - DecimalType::class => $column->getPrecision().','.$column->getScale(), - default => $column->getLength(), - }; - - if ($details) { - return sprintf('%s(%s)%s', $name, $details, $unsigned); - } - - return sprintf('%s%s', $name, $unsigned); - } - /** * Get the default value for the given column. * - * @param \Doctrine\DBAL\Schema\Column $column + * @param array $column * @param \Illuminate\Database\Eloquent\Model $model * @return mixed|null */ protected function getColumnDefault($column, $model) { - $attributeDefault = $model->getAttributes()[$column->getName()] ?? null; + $attributeDefault = $model->getAttributes()[$column['name']] ?? null; return match (true) { $attributeDefault instanceof BackedEnum => $attributeDefault->value, $attributeDefault instanceof UnitEnum => $attributeDefault->name, - default => $attributeDefault ?? $column->getDefault(), + default => $attributeDefault ?? $column['default'], }; } @@ -494,14 +465,14 @@ protected function attributeIsHidden($attribute, $model) * Determine if the given attribute is unique. * * @param string $column - * @param \Doctrine\DBAL\Schema\Index[] $indexes + * @param array $indexes * @return bool */ protected function columnIsUnique($column, $indexes) { - return collect($indexes) - ->filter(fn (Index $index) => count($index->getColumns()) === 1 && $index->getColumns()[0] === $column) - ->contains(fn (Index $index) => $index->isUnique()); + return collect($indexes)->contains( + fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique'] + ); } /** diff --git a/src/Illuminate/Database/Console/TableCommand.php b/src/Illuminate/Database/Console/TableCommand.php index 1a7407d5791b..0bd67e1fdba1 100644 --- a/src/Illuminate/Database/Console/TableCommand.php +++ b/src/Illuminate/Database/Console/TableCommand.php @@ -2,12 +2,10 @@ namespace Illuminate\Database\Console; -use Doctrine\DBAL\Schema\Column; -use Doctrine\DBAL\Schema\ForeignKeyConstraint; -use Doctrine\DBAL\Schema\Index; -use Doctrine\DBAL\Schema\Table; use Illuminate\Database\ConnectionResolverInterface; -use Illuminate\Support\Str; +use Illuminate\Database\Schema\Builder; +use Illuminate\Support\Arr; +use Illuminate\Support\Number; use Symfony\Component\Console\Attribute\AsCommand; use function Laravel\Prompts\select; @@ -39,36 +37,34 @@ class TableCommand extends DatabaseInspectionCommand */ public function handle(ConnectionResolverInterface $connections) { - if (! $this->ensureDependenciesExist()) { - return 1; - } - $connection = $connections->connection($this->input->getOption('database')); + $schema = $connection->getSchemaBuilder(); + $tables = $schema->getTables(); - $schema = $connection->getDoctrineSchemaManager(); - - $this->registerTypeMappings($connection->getDoctrineConnection()->getDatabasePlatform()); - - $table = $this->argument('table') ?: select( + $tableName = $this->argument('table') ?: select( 'Which table would you like to inspect?', - collect($schema->listTables())->flatMap(fn (Table $table) => [$table->getName()])->toArray() + array_column($tables, 'name') ); - if (! $schema->tablesExist([$table])) { - return $this->components->warn("Table [{$table}] doesn't exist."); + $table = Arr::first($tables, fn ($table) => $table['name'] === $tableName); + + if (! $table) { + $this->components->warn("Table [{$table}] doesn't exist."); + + return 1; } - $table = $schema->introspectTable($table); + $tableName = $this->withoutTablePrefix($connection, $table['name']); - $columns = $this->columns($table); - $indexes = $this->indexes($table); - $foreignKeys = $this->foreignKeys($table); + $columns = $this->columns($schema, $tableName); + $indexes = $this->indexes($schema, $tableName); + $foreignKeys = $this->foreignKeys($schema, $tableName); $data = [ 'table' => [ - 'name' => $table->getName(), - 'columns' => $columns->count(), - 'size' => $this->getTableSize($connection, $table->getName()), + 'name' => $table['name'], + 'columns' => count($columns), + 'size' => $table['size'], ], 'columns' => $columns, 'indexes' => $indexes, @@ -83,46 +79,48 @@ public function handle(ConnectionResolverInterface $connections) /** * Get the information regarding the table's columns. * - * @param \Doctrine\DBAL\Schema\Table $table + * @param \Illuminate\Database\Schema\Builder $schema + * @param string $table * @return \Illuminate\Support\Collection */ - protected function columns(Table $table) + protected function columns(Builder $schema, string $table) { - return collect($table->getColumns())->map(fn (Column $column) => [ - 'column' => $column->getName(), + return collect($schema->getColumns($table))->map(fn ($column) => [ + 'column' => $column['name'], 'attributes' => $this->getAttributesForColumn($column), - 'default' => $column->getDefault(), - 'type' => $column->getType()->getName(), + 'default' => $column['default'], + 'type' => $column['type'], ]); } /** * Get the attributes for a table column. * - * @param \Doctrine\DBAL\Schema\Column $column + * @param array $column * @return \Illuminate\Support\Collection */ - protected function getAttributesForColumn(Column $column) + protected function getAttributesForColumn($column) { return collect([ - $column->getAutoincrement() ? 'autoincrement' : null, - 'type' => $column->getType()->getName(), - $column->getUnsigned() ? 'unsigned' : null, - ! $column->getNotNull() ? 'nullable' : null, + $column['type_name'], + $column['auto_increment'] ? 'autoincrement' : null, + $column['nullable'] ? 'nullable' : null, + $column['collation'], ])->filter(); } /** * Get the information regarding the table's indexes. * - * @param \Doctrine\DBAL\Schema\Table $table + * @param \Illuminate\Database\Schema\Builder $schema + * @param string $table * @return \Illuminate\Support\Collection */ - protected function indexes(Table $table) + protected function indexes(Builder $schema, string $table) { - return collect($table->getIndexes())->map(fn (Index $index) => [ - 'name' => $index->getName(), - 'columns' => collect($index->getColumns()), + return collect($schema->getIndexes($table))->map(fn ($index) => [ + 'name' => $index['name'], + 'columns' => collect($index['columns']), 'attributes' => $this->getAttributesForIndex($index), ]); } @@ -130,34 +128,36 @@ protected function indexes(Table $table) /** * Get the attributes for a table index. * - * @param \Doctrine\DBAL\Schema\Index $index + * @param array $index * @return \Illuminate\Support\Collection */ - protected function getAttributesForIndex(Index $index) + protected function getAttributesForIndex($index) { return collect([ - 'compound' => count($index->getColumns()) > 1, - 'unique' => $index->isUnique(), - 'primary' => $index->isPrimary(), - ])->filter()->keys()->map(fn ($attribute) => Str::lower($attribute)); + $index['type'], + count($index['columns']) > 1 ? 'compound' : null, + $index['unique'] && ! $index['primary'] ? 'unique' : null, + $index['primary'] ? 'primary' : null, + ])->filter(); } /** * Get the information regarding the table's foreign keys. * - * @param \Doctrine\DBAL\Schema\Table $table + * @param \Illuminate\Database\Schema\Builder $schema + * @param string $table * @return \Illuminate\Support\Collection */ - protected function foreignKeys(Table $table) + protected function foreignKeys(Builder $schema, string $table) { - return collect($table->getForeignKeys())->map(fn (ForeignKeyConstraint $foreignKey) => [ - 'name' => $foreignKey->getName(), - 'local_table' => $table->getName(), - 'local_columns' => collect($foreignKey->getLocalColumns()), - 'foreign_table' => $foreignKey->getForeignTableName(), - 'foreign_columns' => collect($foreignKey->getForeignColumns()), - 'on_update' => Str::lower(rescue(fn () => $foreignKey->getOption('onUpdate'), 'N/A')), - 'on_delete' => Str::lower(rescue(fn () => $foreignKey->getOption('onDelete'), 'N/A')), + return collect($schema->getForeignKeys($table))->map(fn ($foreignKey) => [ + 'name' => $foreignKey['name'], + 'columns' => collect($foreignKey['columns']), + 'foreign_schema' => $foreignKey['foreign_schema'], + 'foreign_table' => $foreignKey['foreign_table'], + 'foreign_columns' => collect($foreignKey['foreign_columns']), + 'on_update' => $foreignKey['on_update'], + 'on_delete' => $foreignKey['on_delete'], ]); } @@ -201,7 +201,7 @@ protected function displayForCli(array $data) $this->components->twoColumnDetail('Columns', $table['columns']); if ($size = $table['size']) { - $this->components->twoColumnDetail('Size', number_format($size / 1024 / 1024, 2).'MiB'); + $this->components->twoColumnDetail('Size', Number::fileSize($size, 2)); } $this->newLine(); @@ -212,7 +212,7 @@ protected function displayForCli(array $data) $columns->each(function ($column) { $this->components->twoColumnDetail( $column['column'].' '.$column['attributes']->implode(', ').'', - ($column['default'] ? ''.$column['default'].' ' : '').''.$column['type'].'' + (! is_null($column['default']) ? ''.$column['default'].' ' : '').$column['type'] ); }); @@ -237,7 +237,7 @@ protected function displayForCli(array $data) $foreignKeys->each(function ($foreignKey) { $this->components->twoColumnDetail( - $foreignKey['name'].' '.$foreignKey['local_columns']->implode(', ').' references '.$foreignKey['foreign_columns']->implode(', ').' on '.$foreignKey['foreign_table'].'', + $foreignKey['name'].' '.$foreignKey['columns']->implode(', ').' references '.$foreignKey['foreign_columns']->implode(', ').' on '.$foreignKey['foreign_table'].'', $foreignKey['on_update'].' / '.$foreignKey['on_delete'], ); }); diff --git a/src/Illuminate/Database/DBAL/TimestampType.php b/src/Illuminate/Database/DBAL/TimestampType.php deleted file mode 100644 index aee4a2a0130b..000000000000 --- a/src/Illuminate/Database/DBAL/TimestampType.php +++ /dev/null @@ -1,94 +0,0 @@ - $this->getMySqlPlatformSQLDeclaration($column), - PostgreSQLPlatform::class => $this->getPostgresPlatformSQLDeclaration($column), - SQLServerPlatform::class => $this->getSqlServerPlatformSQLDeclaration($column), - SQLitePlatform::class => 'DATETIME', - default => throw NotSupported::new('TIMESTAMP'), - }; - } - - /** - * Get the SQL declaration for MySQL. - * - * @param array $column - * @return string - */ - protected function getMySqlPlatformSQLDeclaration(array $column): string - { - $columnType = 'TIMESTAMP'; - - if ($column['precision']) { - $columnType = 'TIMESTAMP('.min((int) $column['precision'], 6).')'; - } - - $notNull = $column['notnull'] ?? false; - - if (! $notNull) { - return $columnType.' NULL'; - } - - return $columnType; - } - - /** - * Get the SQL declaration for PostgreSQL. - * - * @param array $column - * @return string - */ - protected function getPostgresPlatformSQLDeclaration(array $column): string - { - return 'TIMESTAMP('.min((int) $column['precision'], 6).')'; - } - - /** - * Get the SQL declaration for SQL Server. - * - * @param array $column - * @return string - */ - protected function getSqlServerPlatformSQLDeclaration(array $column): string - { - return $column['precision'] ?? false - ? 'DATETIME2('.min((int) $column['precision'], 7).')' - : 'DATETIME'; - } - - /** - * {@inheritdoc} - * - * @return string - */ - public function getName() - { - return 'timestamp'; - } -} diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php index 84602917ea1a..9239fca06f90 100755 --- a/src/Illuminate/Database/DatabaseManager.php +++ b/src/Illuminate/Database/DatabaseManager.php @@ -2,7 +2,6 @@ namespace Illuminate\Database; -use Doctrine\DBAL\Types\Type; use Illuminate\Database\Connectors\ConnectionFactory; use Illuminate\Database\Events\ConnectionEstablished; use Illuminate\Support\Arr; @@ -57,13 +56,6 @@ class DatabaseManager implements ConnectionResolverInterface */ protected $reconnector; - /** - * The custom Doctrine column types. - * - * @var array - */ - protected $doctrineTypes = []; - /** * Create a new database manager instance. * @@ -101,16 +93,39 @@ public function connection($name = null) $this->makeConnection($database), $type ); - if ($this->app->bound('events')) { - $this->app['events']->dispatch( - new ConnectionEstablished($this->connections[$name]) - ); - } + $this->dispatchConnectionEstablishedEvent($this->connections[$name]); } return $this->connections[$name]; } + /** + * Get a database connection instance from the given configuration. + * + * @param string $name + * @param array $config + * @param bool $force + * @return \Illuminate\Database\ConnectionInterface + */ + public function connectUsing(string $name, array $config, bool $force = false) + { + if ($force) { + $this->purge($name); + } + + if (isset($this->connections[$name])) { + throw new RuntimeException("Cannot establish connection [$name] because another connection with that name already exists."); + } + + $connection = $this->configure( + $this->factory->make($config, $name), null + ); + + $this->dispatchConnectionEstablishedEvent($connection); + + return tap($connection, fn ($connection) => $this->connections[$name] = $connection); + } + /** * Parse the connection into an array of the name and read / write type. * @@ -204,70 +219,42 @@ protected function configure(Connection $connection, $type) // the connection, which will allow us to reconnect from the connections. $connection->setReconnector($this->reconnector); - $this->registerConfiguredDoctrineTypes($connection); - - return $connection; - } - - /** - * Prepare the read / write mode for database connection instance. - * - * @param \Illuminate\Database\Connection $connection - * @param string|null $type - * @return \Illuminate\Database\Connection - */ - protected function setPdoForType(Connection $connection, $type = null) - { - if ($type === 'read') { - $connection->setPdo($connection->getReadPdo()); - } elseif ($type === 'write') { - $connection->setReadPdo($connection->getPdo()); - } - return $connection; } /** - * Register custom Doctrine types with the connection. + * Dispatch the ConnectionEstablished event if the event dispatcher is available. * * @param \Illuminate\Database\Connection $connection * @return void */ - protected function registerConfiguredDoctrineTypes(Connection $connection): void + protected function dispatchConnectionEstablishedEvent(Connection $connection) { - foreach ($this->app['config']->get('database.dbal.types', []) as $name => $class) { - $this->registerDoctrineType($class, $name, $name); + if (! $this->app->bound('events')) { + return; } - foreach ($this->doctrineTypes as $name => [$type, $class]) { - $connection->registerDoctrineType($class, $name, $type); - } + $this->app['events']->dispatch( + new ConnectionEstablished($connection) + ); } /** - * Register a custom Doctrine type. - * - * @param string $class - * @param string $name - * @param string $type - * @return void + * Prepare the read / write mode for database connection instance. * - * @throws \Doctrine\DBAL\Exception - * @throws \RuntimeException + * @param \Illuminate\Database\Connection $connection + * @param string|null $type + * @return \Illuminate\Database\Connection */ - public function registerDoctrineType(string $class, string $name, string $type): void + protected function setPdoForType(Connection $connection, $type = null) { - if (! class_exists('Doctrine\DBAL\Connection')) { - throw new RuntimeException( - 'Registering a custom Doctrine type requires Doctrine DBAL (doctrine/dbal).' - ); - } - - if (! Type::hasType($name)) { - Type::addType($name, $class); + if ($type === 'read') { + $connection->setPdo($connection->getReadPdo()); + } elseif ($type === 'write') { + $connection->setReadPdo($connection->getPdo()); } - $this->doctrineTypes[$name] = [$type, $class]; + return $connection; } /** @@ -374,13 +361,13 @@ public function setDefaultConnection($name) } /** - * Get all of the support drivers. + * Get all of the supported drivers. * * @return string[] */ public function supportedDrivers() { - return ['mysql', 'pgsql', 'sqlite', 'sqlsrv']; + return ['mysql', 'mariadb', 'pgsql', 'sqlite', 'sqlsrv']; } /** diff --git a/src/Illuminate/Database/DatabaseTransactionsManager.php b/src/Illuminate/Database/DatabaseTransactionsManager.php index c730dc503ac2..ee2889a2d18a 100755 --- a/src/Illuminate/Database/DatabaseTransactionsManager.php +++ b/src/Illuminate/Database/DatabaseTransactionsManager.php @@ -83,7 +83,8 @@ public function commit($connection, $levelBeingCommitted, $newTransactionLevel) // shouldn't be any pending transactions, but going to clear them here anyways just // in case. This method could be refactored to receive a level in the future too. $this->pendingTransactions = $this->pendingTransactions->reject( - fn ($transaction) => $transaction->connection === $connection + fn ($transaction) => $transaction->connection === $connection && + $transaction->level >= $levelBeingCommitted )->values(); [$forThisConnection, $forOtherConnections] = $this->committedTransactions->partition( diff --git a/src/Illuminate/Database/Eloquent/Attributes/ObservedBy.php b/src/Illuminate/Database/Eloquent/Attributes/ObservedBy.php new file mode 100644 index 000000000000..600174146f94 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Attributes/ObservedBy.php @@ -0,0 +1,19 @@ +contains('or')) { + if ($whereBooleans->contains(fn ($logicalOperator) => str_contains($logicalOperator, 'or'))) { $query->wheres[] = $this->createNestedWhere( - $whereSlice, $whereBooleans->first() + $whereSlice, str_replace(' not', '', $whereBooleans->first()) ); } else { $query->wheres = array_merge($query->wheres, $whereSlice); diff --git a/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php index 2ee6b56e0901..5ee80d0bb4f0 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php @@ -25,7 +25,7 @@ public function get($model, $key, $value, $attributes) $data = Json::decode($attributes[$key]); - return is_array($data) ? new ArrayObject($data) : null; + return is_array($data) ? new ArrayObject($data, ArrayObject::ARRAY_AS_PROPS) : null; } public function set($model, $key, $value, $attributes) diff --git a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php index b7e0d7dea8c0..f7d4c9ff538d 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php @@ -7,7 +7,7 @@ trait GuardsAttributes /** * The attributes that are mass assignable. * - * @var array + * @var array */ protected $fillable = []; diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index a95fb62fb73a..10187f50d701 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1328,7 +1328,7 @@ public function fromJson($value, $asObject = false) */ public function fromEncryptedString($value) { - return (static::$encrypter ?? Crypt::getFacadeRoot())->decrypt($value, false); + return static::currentEncrypter()->decrypt($value, false); } /** @@ -1340,7 +1340,7 @@ public function fromEncryptedString($value) */ protected function castAttributeAsEncryptedString($key, $value) { - return (static::$encrypter ?? Crypt::getFacadeRoot())->encrypt($value, false); + return static::currentEncrypter()->encrypt($value, false); } /** @@ -1354,6 +1354,16 @@ public static function encryptUsing($encrypter) static::$encrypter = $encrypter; } + /** + * Get the current encrypter being used by the model. + * + * @return \Illuminate\Contracts\Encryption\Encrypter + */ + protected static function currentEncrypter() + { + return static::$encrypter ?? Crypt::getFacadeRoot(); + } + /** * Cast the given attribute to a hashed string. * @@ -2145,6 +2155,8 @@ public function originalIsEquivalent($key) } return abs($this->castAttribute($key, $attribute) - $this->castAttribute($key, $original)) < PHP_FLOAT_EPSILON * 4; + } elseif ($this->isEncryptedCastable($key) && ! empty(static::currentEncrypter()->getPreviousKeys())) { + return false; } elseif ($this->hasCast($key, static::$primitiveCastTypes)) { return $this->castAttribute($key, $attribute) === $this->castAttribute($key, $original); @@ -2153,7 +2165,11 @@ public function originalIsEquivalent($key) } elseif ($this->isClassCastable($key) && Str::startsWith($this->getCasts()[$key], [AsEnumArrayObject::class, AsEnumCollection::class])) { return $this->fromJson($attribute) === $this->fromJson($original); } elseif ($this->isClassCastable($key) && $original !== null && Str::startsWith($this->getCasts()[$key], [AsEncryptedArrayObject::class, AsEncryptedCollection::class])) { - return $this->fromEncryptedString($attribute) === $this->fromEncryptedString($original); + if (empty(static::currentEncrypter()->getPreviousKeys())) { + return $this->fromEncryptedString($attribute) === $this->fromEncryptedString($original); + } + + return false; } return is_numeric($attribute) && is_numeric($original) @@ -2182,6 +2198,13 @@ protected function transformModelValue($key, $value) // an appropriate native PHP type dependent upon the associated value // given with the key in the pair. Dayle made this comment line up. if ($this->hasCast($key)) { + if (static::preventsAccessingMissingAttributes() && + ! array_key_exists($key, $this->attributes) && + ($this->isEnumCastable($key) || + in_array($this->getCastType($key), static::$primitiveCastTypes))) { + $this->throwMissingAttributeExceptionIfApplicable($key); + } + return $this->castAttribute($key, $value); } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php index 37bc063aaa85..0730dcb10971 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasEvents.php @@ -3,9 +3,11 @@ namespace Illuminate\Database\Eloquent\Concerns; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Database\Eloquent\Attributes\ObservedBy; use Illuminate\Events\NullDispatcher; use Illuminate\Support\Arr; use InvalidArgumentException; +use ReflectionClass; trait HasEvents { @@ -27,6 +29,31 @@ trait HasEvents */ protected $observables = []; + /** + * Boot the has event trait for a model. + * + * @return void + */ + public static function bootHasEvents() + { + static::observe(static::resolveObserveAttributes()); + } + + /** + * Resolve the observe class names from the attributes. + * + * @return array + */ + public static function resolveObserveAttributes() + { + $reflectionClass = new ReflectionClass(static::class); + + return collect($reflectionClass->getAttributes(ObservedBy::class)) + ->map(fn ($attribute) => $attribute->getArguments()) + ->flatten() + ->all(); + } + /** * Register observers with the model. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php index 5d7047953115..0913d94b372a 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php @@ -3,12 +3,39 @@ namespace Illuminate\Database\Eloquent\Concerns; use Closure; +use Illuminate\Database\Eloquent\Attributes\ScopedBy; use Illuminate\Database\Eloquent\Scope; use Illuminate\Support\Arr; use InvalidArgumentException; +use ReflectionClass; trait HasGlobalScopes { + /** + * Boot the has global scopes trait for a model. + * + * @return void + */ + public static function bootHasGlobalScopes() + { + static::addGlobalScopes(static::resolveGlobalScopeAttributes()); + } + + /** + * Resolve the global scope class names from the attributes. + * + * @return array + */ + public static function resolveGlobalScopeAttributes() + { + $reflectionClass = new ReflectionClass(static::class); + + return collect($reflectionClass->getAttributes(ScopedBy::class)) + ->map(fn ($attribute) => $attribute->getArguments()) + ->flatten() + ->all(); + } + /** * Register a new global scope on the model. * @@ -26,9 +53,28 @@ public static function addGlobalScope($scope, $implementation = null) return static::$globalScopes[static::class][spl_object_hash($scope)] = $scope; } elseif ($scope instanceof Scope) { return static::$globalScopes[static::class][get_class($scope)] = $scope; + } elseif (is_string($scope) && class_exists($scope) && is_subclass_of($scope, Scope::class)) { + return static::$globalScopes[static::class][$scope] = new $scope; } - throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope.'); + throw new InvalidArgumentException('Global scope must be an instance of Closure or Scope or be a class name of a class extending '.Scope::class); + } + + /** + * Register multiple global scopes on the model. + * + * @param array $scopes + * @return void + */ + public static function addGlobalScopes(array $scopes) + { + foreach ($scopes as $key => $scope) { + if (is_string($key)) { + static::addGlobalScope($key, $scope); + } else { + static::addGlobalScope($scope); + } + } } /** diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 154717fa4d81..de43f9839824 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -599,7 +599,7 @@ public function orWhereBelongsTo($related, $relationshipName = null) * Add subselect queries to include an aggregate value for a relationship. * * @param mixed $relations - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @param string $function * @return $this */ @@ -630,15 +630,19 @@ public function withAggregate($relations, $column, $function = null) $relation = $this->getRelationWithoutConstraints($name); if ($function) { - $hashedColumn = $this->getRelationHashedColumn($column, $relation); + if ($this->getQuery()->getGrammar()->isExpression($column)) { + $aggregateColumn = $this->getQuery()->getGrammar()->getValue($column); + } else { + $hashedColumn = $this->getRelationHashedColumn($column, $relation); - $wrappedColumn = $this->getQuery()->getGrammar()->wrap( - $column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn) - ); + $aggregateColumn = $this->getQuery()->getGrammar()->wrap( + $column === '*' ? $column : $relation->getRelated()->qualifyColumn($hashedColumn) + ); + } - $expression = $function === 'exists' ? $wrappedColumn : sprintf('%s(%s)', $function, $wrappedColumn); + $expression = $function === 'exists' ? $aggregateColumn : sprintf('%s(%s)', $function, $aggregateColumn); } else { - $expression = $column; + $expression = $this->getQuery()->getGrammar()->getValue($column); } // Here, we will grab the relationship sub-query and prepare to add it to the main query @@ -667,7 +671,7 @@ public function withAggregate($relations, $column, $function = null) // the query builder. Then, we will return the builder instance back to the developer // for further constraint chaining that needs to take place on the query as needed. $alias ??= Str::snake( - preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function $column") + preg_replace('/[^[:alnum:][:space:]_]/u', '', "$name $function {$this->getQuery()->getGrammar()->getValue($column)}") ); if ($function === 'exists') { @@ -719,7 +723,7 @@ public function withCount($relations) * Add subselect queries to include the max of the relation's column. * * @param string|array $relation - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withMax($relation, $column) @@ -731,7 +735,7 @@ public function withMax($relation, $column) * Add subselect queries to include the min of the relation's column. * * @param string|array $relation - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withMin($relation, $column) @@ -743,7 +747,7 @@ public function withMin($relation, $column) * Add subselect queries to include the sum of the relation's column. * * @param string|array $relation - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withSum($relation, $column) @@ -755,7 +759,7 @@ public function withSum($relation, $column) * Add subselect queries to include the average of the relation's column. * * @param string|array $relation - * @param string $column + * @param \Illuminate\Contracts\Database\Query\Expression|string $column * @return $this */ public function withAvg($relation, $column) diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index 37c698f3d80f..9fe0d301918b 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -11,6 +11,7 @@ use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot; use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithPivotTable; +use Illuminate\Database\Query\Grammars\MySqlGrammar; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Support\Str; use InvalidArgumentException; @@ -1179,6 +1180,10 @@ protected function guessInverseRelation() */ public function touch() { + if ($this->related->isIgnoringTouch()) { + return; + } + $columns = [ $this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(), ]; @@ -1347,6 +1352,42 @@ public function getRelationExistenceQueryForSelfJoin(Builder $query, Builder $pa return parent::getRelationExistenceQuery($query, $parentQuery, $columns); } + /** + * Alias to set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function take($value) + { + return $this->limit($value); + } + + /** + * Set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function limit($value) + { + if ($this->parent->exists) { + $this->query->limit($value); + } else { + $column = $this->getExistenceCompareKey(); + + $grammar = $this->query->getQuery()->getGrammar(); + + if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) { + $column = 'pivot_'.last(explode('.', $column)); + } + + $this->query->groupLimit($value, $column); + } + + return $this; + } + /** * Get the key for comparing against the parent key in "has" query. * diff --git a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php index b0b4b1fdebe1..55f9aacd1e2c 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php @@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Eloquent\Relations\Concerns\InteractsWithDictionary; use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Database\Query\Grammars\MySqlGrammar; use Illuminate\Database\UniqueConstraintViolationException; class HasManyThrough extends Relation @@ -762,6 +763,42 @@ public function getRelationExistenceQueryForThroughSelfRelation(Builder $query, ); } + /** + * Alias to set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function take($value) + { + return $this->limit($value); + } + + /** + * Set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function limit($value) + { + if ($this->farParent->exists) { + $this->query->limit($value); + } else { + $column = $this->getQualifiedFirstKeyName(); + + $grammar = $this->query->getQuery()->getGrammar(); + + if ($grammar instanceof MySqlGrammar && $grammar->useLegacyGroupLimit($this->query->getQuery())) { + $column = 'laravel_through_key'; + } + + $this->query->groupLimit($value, $column); + } + + return $this; + } + /** * Get the qualified foreign key on the related model. * diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index af263baf854f..e1d295d86be4 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -453,6 +453,34 @@ public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder ); } + /** + * Alias to set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function take($value) + { + return $this->limit($value); + } + + /** + * Set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function limit($value) + { + if ($this->parent->exists) { + $this->query->limit($value); + } else { + $this->query->groupLimit($value, $this->getExistenceCompareKey()); + } + + return $this; + } + /** * Get the key for comparing against the parent key in "has" query. * diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php index 5ca8b48bed02..39c7852f2888 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php @@ -68,6 +68,8 @@ public function delete() $query->where($this->morphType, $this->morphClass); return tap($query->delete(), function () { + $this->exists = false; + $this->fireModelEvent('deleted', false); }); } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php index 87b8e7816f9f..8cf113bd0f34 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php @@ -189,6 +189,16 @@ public function getMorphType() return $this->morphType; } + /** + * Get the fully qualified morph type for the relation. + * + * @return string + */ + public function getQualifiedMorphTypeName() + { + return $this->qualifyPivotColumn($this->morphType); + } + /** * Get the class name of the parent model. * diff --git a/src/Illuminate/Database/Eloquent/Relations/Pivot.php b/src/Illuminate/Database/Eloquent/Relations/Pivot.php index a65ecdea6633..6e1d3f27897e 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Pivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/Pivot.php @@ -19,7 +19,7 @@ class Pivot extends Model /** * The attributes that aren't mass assignable. * - * @var array + * @var array|bool */ protected $guarded = []; } diff --git a/src/Illuminate/Database/Grammar.php b/src/Illuminate/Database/Grammar.php index 9ce0ec352595..79da1724ccb4 100755 --- a/src/Illuminate/Database/Grammar.php +++ b/src/Illuminate/Database/Grammar.php @@ -43,21 +43,38 @@ public function wrapArray(array $values) */ public function wrapTable($table) { - if (! $this->isExpression($table)) { - return $this->wrap($this->tablePrefix.$table, true); + if ($this->isExpression($table)) { + return $this->getValue($table); } - return $this->getValue($table); + // If the table being wrapped has an alias we'll need to separate the pieces + // so we can prefix the table and then wrap each of the segments on their + // own and then join these both back together using the "as" connector. + if (stripos($table, ' as ') !== false) { + return $this->wrapAliasedTable($table); + } + + // If the table being wrapped has a custom schema name specified, we need to + // prefix the last segment as the table name then wrap each segment alone + // and eventually join them both back together using the dot connector. + if (str_contains($table, '.')) { + $table = substr_replace($table, '.'.$this->tablePrefix, strrpos($table, '.'), 1); + + return collect(explode('.', $table)) + ->map($this->wrapValue(...)) + ->implode('.'); + } + + return $this->wrapValue($this->tablePrefix.$table); } /** * Wrap a value in keyword identifiers. * * @param \Illuminate\Contracts\Database\Query\Expression|string $value - * @param bool $prefixAlias * @return string */ - public function wrap($value, $prefixAlias = false) + public function wrap($value) { if ($this->isExpression($value)) { return $this->getValue($value); @@ -67,7 +84,7 @@ public function wrap($value, $prefixAlias = false) // the pieces so we can wrap each of the segments of the expression on its // own, and then join these both back together using the "as" connector. if (stripos($value, ' as ') !== false) { - return $this->wrapAliasedValue($value, $prefixAlias); + return $this->wrapAliasedValue($value); } // If the given value is a JSON selector we will wrap it differently than a @@ -84,23 +101,28 @@ public function wrap($value, $prefixAlias = false) * Wrap a value that has an alias. * * @param string $value - * @param bool $prefixAlias * @return string */ - protected function wrapAliasedValue($value, $prefixAlias = false) + protected function wrapAliasedValue($value) { $segments = preg_split('/\s+as\s+/i', $value); - // If we are wrapping a table we need to prefix the alias with the table prefix - // as well in order to generate proper syntax. If this is a column of course - // no prefix is necessary. The condition will be true when from wrapTable. - if ($prefixAlias) { - $segments[1] = $this->tablePrefix.$segments[1]; - } - return $this->wrap($segments[0]).' as '.$this->wrapValue($segments[1]); } + /** + * Wrap a table that has an alias. + * + * @param string $value + * @return string + */ + protected function wrapAliasedTable($value) + { + $segments = preg_split('/\s+as\s+/i', $value); + + return $this->wrapTable($segments[0]).' as '.$this->wrapValue($this->tablePrefix.$segments[1]); + } + /** * Wrap the given value segments. * diff --git a/src/Illuminate/Database/MariaDbConnection.php b/src/Illuminate/Database/MariaDbConnection.php new file mode 100755 index 000000000000..58e124d50c10 --- /dev/null +++ b/src/Illuminate/Database/MariaDbConnection.php @@ -0,0 +1,94 @@ +setConnection($this); + + return $this->withTablePrefix($grammar); + } + + /** + * Get a schema builder instance for the connection. + * + * @return \Illuminate\Database\Schema\MariaDbBuilder + */ + public function getSchemaBuilder() + { + if (is_null($this->schemaGrammar)) { + $this->useDefaultSchemaGrammar(); + } + + return new MariaDbBuilder($this); + } + + /** + * Get the default schema grammar instance. + * + * @return \Illuminate\Database\Schema\Grammars\MariaDbGrammar + */ + protected function getDefaultSchemaGrammar() + { + ($grammar = new SchemaGrammar)->setConnection($this); + + return $this->withTablePrefix($grammar); + } + + /** + * Get the schema state for the connection. + * + * @param \Illuminate\Filesystem\Filesystem|null $files + * @param callable|null $processFactory + * @return \Illuminate\Database\Schema\MariaDbSchemaState + */ + public function getSchemaState(Filesystem $files = null, callable $processFactory = null) + { + return new MariaDbSchemaState($this, $files, $processFactory); + } + + /** + * Get the default post processor instance. + * + * @return \Illuminate\Database\Query\Processors\MariaDbProcessor + */ + protected function getDefaultPostProcessor() + { + return new MariaDbProcessor; + } +} diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 4f6ec5247a04..e650959419f9 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -2,9 +2,7 @@ namespace Illuminate\Database\Migrations; -use Doctrine\DBAL\Schema\SchemaException; use Illuminate\Console\View\Components\BulletList; -use Illuminate\Console\View\Components\Error; use Illuminate\Console\View\Components\Info; use Illuminate\Console\View\Components\Task; use Illuminate\Console\View\Components\TwoColumnDetail; @@ -428,28 +426,19 @@ protected function runMigration($migration, $method) */ protected function pretendToRun($migration, $method) { - try { - $name = get_class($migration); - - $reflectionClass = new ReflectionClass($migration); + $name = get_class($migration); - if ($reflectionClass->isAnonymous()) { - $name = $this->getMigrationName($reflectionClass->getFileName()); - } + $reflectionClass = new ReflectionClass($migration); - $this->write(TwoColumnDetail::class, $name); + if ($reflectionClass->isAnonymous()) { + $name = $this->getMigrationName($reflectionClass->getFileName()); + } - $this->write(BulletList::class, collect($this->getQueries($migration, $method))->map(function ($query) { - return $query['query']; - })); - } catch (SchemaException) { - $name = get_class($migration); + $this->write(TwoColumnDetail::class, $name); - $this->write(Error::class, sprintf( - '[%s] failed to dump queries. This may be due to changing database columns using Doctrine, which is not supported while pretending to run migrations.', - $name, - )); - } + $this->write(BulletList::class, collect($this->getQueries($migration, $method))->map(function ($query) { + return $query['query']; + })); } /** diff --git a/src/Illuminate/Database/MySqlConnection.php b/src/Illuminate/Database/MySqlConnection.php index 460a4fd375c1..00d212e9481d 100755 --- a/src/Illuminate/Database/MySqlConnection.php +++ b/src/Illuminate/Database/MySqlConnection.php @@ -3,13 +3,13 @@ namespace Illuminate\Database; use Exception; -use Illuminate\Database\PDO\MySqlDriver; use Illuminate\Database\Query\Grammars\MySqlGrammar as QueryGrammar; use Illuminate\Database\Query\Processors\MySqlProcessor; use Illuminate\Database\Schema\Grammars\MySqlGrammar as SchemaGrammar; use Illuminate\Database\Schema\MySqlBuilder; use Illuminate\Database\Schema\MySqlSchemaState; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Str; use PDO; class MySqlConnection extends Connection @@ -48,6 +48,18 @@ public function isMaria() return str_contains($this->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), 'MariaDB'); } + /** + * Get the server version for the connection. + * + * @return string + */ + public function getServerVersion(): string + { + return str_contains($version = parent::getServerVersion(), 'MariaDB') + ? Str::between($version, '5.5.5-', '-MariaDB') + : $version; + } + /** * Get the default query grammar instance. * @@ -107,14 +119,4 @@ protected function getDefaultPostProcessor() { return new MySqlProcessor; } - - /** - * Get the Doctrine DBAL driver. - * - * @return \Illuminate\Database\PDO\MySqlDriver - */ - protected function getDoctrineDriver() - { - return new MySqlDriver; - } } diff --git a/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php b/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php deleted file mode 100644 index a2354182bc2c..000000000000 --- a/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php +++ /dev/null @@ -1,28 +0,0 @@ -connection = $connection; - } - - /** - * Execute an SQL statement. - * - * @param string $statement - * @return int - */ - public function exec(string $statement): int - { - try { - $result = $this->connection->exec($statement); - - \assert($result !== false); - - return $result; - } catch (PDOException $exception) { - throw Exception::new($exception); - } - } - - /** - * Prepare a new SQL statement. - * - * @param string $sql - * @return \Doctrine\DBAL\Driver\Statement - * - * @throws \Doctrine\DBAL\Driver\PDO\Exception - */ - public function prepare(string $sql): StatementInterface - { - try { - return $this->createStatement( - $this->connection->prepare($sql) - ); - } catch (PDOException $exception) { - throw Exception::new($exception); - } - } - - /** - * Execute a new query against the connection. - * - * @param string $sql - * @return \Doctrine\DBAL\Driver\Result - */ - public function query(string $sql): ResultInterface - { - try { - $stmt = $this->connection->query($sql); - - \assert($stmt instanceof PDOStatement); - - return new Result($stmt); - } catch (PDOException $exception) { - throw Exception::new($exception); - } - } - - /** - * Get the last insert ID. - * - * @param string|null $name - * @return string|int - * - * @throws \Doctrine\DBAL\Driver\PDO\Exception - */ - public function lastInsertId($name = null): string|int - { - try { - if ($name === null) { - return $this->connection->lastInsertId(); - } - - return $this->connection->lastInsertId($name); - } catch (PDOException $exception) { - throw Exception::new($exception); - } - } - - /** - * Create a new statement instance. - * - * @param \PDOStatement $stmt - * @return \Doctrine\DBAL\Driver\PDO\Statement - */ - protected function createStatement(PDOStatement $stmt): Statement - { - return new Statement($stmt); - } - - /** - * Begin a new database transaction. - * - * @return void - */ - public function beginTransaction(): void - { - $this->connection->beginTransaction(); - } - - /** - * Commit a database transaction. - * - * @return void - */ - public function commit(): void - { - $this->connection->commit(); - } - - /** - * Rollback a database transaction. - * - * @return void - */ - public function rollBack(): void - { - $this->connection->rollBack(); - } - - /** - * Wrap quotes around the given input. - * - * @param string $input - * @param string $type - * @return string - */ - public function quote($input, $type = ParameterType::STRING): string - { - return $this->connection->quote($input, $type); - } - - /** - * Get the server version for the connection. - * - * @return string - */ - public function getServerVersion(): string - { - return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); - } - - /** - * Get the native PDO connection. - * - * @return \PDO - */ - public function getNativeConnection(): PDO - { - return $this->connection; - } -} diff --git a/src/Illuminate/Database/PDO/MySqlDriver.php b/src/Illuminate/Database/PDO/MySqlDriver.php deleted file mode 100644 index 54ac37536189..000000000000 --- a/src/Illuminate/Database/PDO/MySqlDriver.php +++ /dev/null @@ -1,19 +0,0 @@ -connection = $connection; - } - - /** - * Prepare a new SQL statement. - * - * @param string $sql - * @return \Doctrine\DBAL\Driver\Statement - */ - public function prepare(string $sql): StatementInterface - { - return new Statement( - $this->connection->prepare($sql) - ); - } - - /** - * Execute a new query against the connection. - * - * @param string $sql - * @return \Doctrine\DBAL\Driver\Result - */ - public function query(string $sql): Result - { - return $this->connection->query($sql); - } - - /** - * Execute an SQL statement. - * - * @param string $statement - * @return int - */ - public function exec(string $statement): int - { - return $this->connection->exec($statement); - } - - /** - * Get the last insert ID. - * - * @param string|null $name - * @return string|int - */ - public function lastInsertId($name = null): string|int - { - if ($name === null) { - return $this->connection->lastInsertId($name); - } - - return $this->prepare('SELECT CONVERT(VARCHAR(MAX), current_value) FROM sys.sequences WHERE name = ?') - ->execute([$name]) - ->fetchOne(); - } - - /** - * Begin a new database transaction. - * - * @return void - */ - public function beginTransaction(): void - { - $this->connection->beginTransaction(); - } - - /** - * Commit a database transaction. - * - * @return void - */ - public function commit(): void - { - $this->connection->commit(); - } - - /** - * Rollback a database transaction. - * - * @return void - */ - public function rollBack(): void - { - $this->connection->rollBack(); - } - - /** - * Wrap quotes around the given input. - * - * @param string $value - * @param int $type - * @return string - */ - public function quote($value, $type = ParameterType::STRING): string - { - $val = $this->connection->quote($value, $type); - - // Fix for a driver version terminating all values with null byte... - if (\is_string($val) && str_contains($val, "\0")) { - $val = \substr($val, 0, -1); - } - - return $val; - } - - /** - * Get the server version for the connection. - * - * @return string - */ - public function getServerVersion(): string - { - return $this->connection->getServerVersion(); - } - - /** - * Get the native PDO connection. - * - * @return \PDO - */ - public function getNativeConnection(): PDO - { - return $this->connection->getWrappedConnection(); - } -} diff --git a/src/Illuminate/Database/PDO/SqlServerDriver.php b/src/Illuminate/Database/PDO/SqlServerDriver.php deleted file mode 100644 index ac7b8a1aedef..000000000000 --- a/src/Illuminate/Database/PDO/SqlServerDriver.php +++ /dev/null @@ -1,30 +0,0 @@ -join(new Expression($expression), $first, $operator, $second, $type, $where); } + /** + * Add a lateral join clause to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @param string $as + * @param string $type + * @return $this + */ + public function joinLateral($query, string $as, string $type = 'inner') + { + [$query, $bindings] = $this->createSub($query); + + $expression = '('.$query.') as '.$this->grammar->wrapTable($as); + + $this->addBinding($bindings, 'join'); + + $this->joins[] = $this->newJoinLateralClause($this, $type, new Expression($expression)); + + return $this; + } + + /** + * Add a lateral left join to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @param string $as + * @return $this + */ + public function leftJoinLateral($query, string $as) + { + return $this->joinLateral($query, $as, 'left'); + } + /** * Add a left join to the query. * @@ -729,6 +769,19 @@ protected function newJoinClause(self $parentQuery, $type, $table) return new JoinClause($parentQuery, $type, $table); } + /** + * Get a new join lateral clause. + * + * @param \Illuminate\Database\Query\Builder $parentQuery + * @param string $type + * @param string $table + * @return \Illuminate\Database\Query\JoinLateralClause + */ + protected function newJoinLateralClause(self $parentQuery, $type, $table) + { + return new JoinLateralClause($parentQuery, $type, $table); + } + /** * Merge an array of where clauses and bindings. * @@ -1383,7 +1436,7 @@ public function orWhereNotNull($column) * Add a "where date" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|null $operator * @param \DateTimeInterface|string|null $value * @param string $boolean * @return $this @@ -1407,7 +1460,7 @@ public function whereDate($column, $operator, $value = null, $boolean = 'and') * Add an "or where date" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|null $operator * @param \DateTimeInterface|string|null $value * @return $this */ @@ -1424,7 +1477,7 @@ public function orWhereDate($column, $operator, $value = null) * Add a "where time" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|null $operator * @param \DateTimeInterface|string|null $value * @param string $boolean * @return $this @@ -1448,7 +1501,7 @@ public function whereTime($column, $operator, $value = null, $boolean = 'and') * Add an "or where time" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|null $operator * @param \DateTimeInterface|string|null $value * @return $this */ @@ -1465,7 +1518,7 @@ public function orWhereTime($column, $operator, $value = null) * Add a "where day" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|int|null $operator * @param \DateTimeInterface|string|int|null $value * @param string $boolean * @return $this @@ -1493,7 +1546,7 @@ public function whereDay($column, $operator, $value = null, $boolean = 'and') * Add an "or where day" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|int|null $operator * @param \DateTimeInterface|string|int|null $value * @return $this */ @@ -1510,7 +1563,7 @@ public function orWhereDay($column, $operator, $value = null) * Add a "where month" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|int|null $operator * @param \DateTimeInterface|string|int|null $value * @param string $boolean * @return $this @@ -1538,7 +1591,7 @@ public function whereMonth($column, $operator, $value = null, $boolean = 'and') * Add an "or where month" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|int|null $operator * @param \DateTimeInterface|string|int|null $value * @return $this */ @@ -1555,7 +1608,7 @@ public function orWhereMonth($column, $operator, $value = null) * Add a "where year" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|int|null $operator * @param \DateTimeInterface|string|int|null $value * @param string $boolean * @return $this @@ -1579,7 +1632,7 @@ public function whereYear($column, $operator, $value = null, $boolean = 'and') * Add an "or where year" statement to the query. * * @param \Illuminate\Contracts\Database\Query\Expression|string $column - * @param string $operator + * @param \DateTimeInterface|string|int|null $operator * @param \DateTimeInterface|string|int|null $value * @return $this */ @@ -2447,6 +2500,22 @@ public function limit($value) return $this; } + /** + * Add a "group limit" clause to the query. + * + * @param int $value + * @param string $column + * @return $this + */ + public function groupLimit($value, $column) + { + if ($value >= 0) { + $this->groupLimit = compact('value', 'column'); + } + + return $this; + } + /** * Set the limit and offset for a given page. * @@ -2740,10 +2809,14 @@ public function soleValue($column) */ public function get($columns = ['*']) { - return collect((clone $this) + $items = collect((clone $this) ->select(is_null($this->columns) ? Arr::wrap($columns) : $this->columns) ->processor->processSelect($this, $this->runSelect()) ); + + return isset($this->groupLimit) + ? $this->withoutGroupLimitKeys($items) + : $items; } /** @@ -2758,6 +2831,32 @@ protected function runSelect() ); } + /** + * Remove the group limit keys from the results in the collection. + * + * @param \Illuminate\Support\Collection $items + * @return \Illuminate\Support\Collection + */ + protected function withoutGroupLimitKeys($items) + { + $keysToRemove = ['laravel_row']; + + if (is_string($this->groupLimit['column'])) { + $column = last(explode('.', $this->groupLimit['column'])); + + $keysToRemove[] = '@laravel_group := '.$this->grammar->wrap($column); + $keysToRemove[] = '@laravel_group := '.$this->grammar->wrap('pivot_'.$column); + } + + $items->each(function ($item) use ($keysToRemove) { + foreach ($keysToRemove as $key) { + unset($item->$key); + } + }); + + return $items; + } + /** * Paginate the given query into a simple paginator. * @@ -3295,6 +3394,25 @@ public function insertUsing(array $columns, $query) ); } + /** + * Insert new records into the table using a subquery while ignoring errors. + * + * @param array $columns + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Eloquent\Builder|string $query + * @return int + */ + public function insertOrIgnoreUsing(array $columns, $query) + { + $this->applyBeforeQueryCallbacks(); + + [$sql, $bindings] = $this->createSub($query); + + return $this->connection->affectingStatement( + $this->grammar->compileInsertOrIgnoreUsing($this, $columns, $sql), + $this->cleanBindings($bindings) + ); + } + /** * Update records in the database. * @@ -3305,10 +3423,20 @@ public function update(array $values) { $this->applyBeforeQueryCallbacks(); - $sql = $this->grammar->compileUpdate($this, $values); + $values = collect($values)->map(function ($value) { + if (! $value instanceof Builder) { + return ['value' => $value, 'bindings' => $value]; + } + + [$query, $bindings] = $this->parseSub($value); + + return ['value' => new Expression("({$query})"), 'bindings' => fn () => $bindings]; + }); + + $sql = $this->grammar->compileUpdate($this, $values->map(fn ($value) => $value['value'])->all()); return $this->connection->update($sql, $this->cleanBindings( - $this->grammar->prepareBindingsForUpdate($this->bindings, $values) + $this->grammar->prepareBindingsForUpdate($this->bindings, $values->map(fn ($value) => $value['bindings'])->all()) )); } diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 3b4f117693f6..f2ce92c28ff3 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -7,6 +7,7 @@ use Illuminate\Database\Grammar as BaseGrammar; use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\JoinClause; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use RuntimeException; @@ -60,6 +61,17 @@ public function compileSelect(Builder $query) return $this->compileUnionAggregate($query); } + // If a "group limit" is in place, we will need to compile the SQL to use a + // different syntax. This primarily supports limits on eager loads using + // Eloquent. We'll also set the columns if they have not been defined. + if (isset($query->groupLimit)) { + if (is_null($query->columns)) { + $query->columns = ['*']; + } + + return $this->compileGroupLimit($query); + } + // If the query does not have any columns set, we'll set the columns to the // * character to just get all of the columns from the database. Then we // can build the query and concatenate all the pieces together as one. @@ -182,10 +194,28 @@ protected function compileJoins(Builder $query, $joins) $tableAndNestedJoins = is_null($join->joins) ? $table : '('.$table.$nestedJoins.')'; + if ($join instanceof JoinLateralClause) { + return $this->compileJoinLateral($join, $tableAndNestedJoins); + } + return trim("{$join->type} join {$tableAndNestedJoins} {$this->compileWheres($join)}"); })->implode(' '); } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + * + * @throws \RuntimeException + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + throw new RuntimeException('This database engine does not support lateral joins.'); + } + /** * Compile the "where" portions of the query. * @@ -917,6 +947,66 @@ protected function compileLimit(Builder $query, $limit) return 'limit '.(int) $limit; } + /** + * Compile a group limit clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @return string + */ + protected function compileGroupLimit(Builder $query) + { + $selectBindings = array_merge($query->getRawBindings()['select'], $query->getRawBindings()['order']); + + $query->setBindings($selectBindings, 'select'); + $query->setBindings([], 'order'); + + $limit = (int) $query->groupLimit['value']; + $offset = $query->offset; + + if (isset($offset)) { + $offset = (int) $offset; + $limit += $offset; + + $query->offset = null; + } + + $components = $this->compileComponents($query); + + $components['columns'] .= $this->compileRowNumber( + $query->groupLimit['column'], + $components['orders'] ?? '' + ); + + unset($components['orders']); + + $table = $this->wrap('laravel_table'); + $row = $this->wrap('laravel_row'); + + $sql = $this->concatenate($components); + + $sql = 'select * from ('.$sql.') as '.$table.' where '.$row.' <= '.$limit; + + if (isset($offset)) { + $sql .= ' and '.$row.' > '.$offset; + } + + return $sql.' order by '.$row; + } + + /** + * Compile a row number clause. + * + * @param string $partition + * @param string $orders + * @return string + */ + protected function compileRowNumber($partition, $orders) + { + $over = trim('partition by '.$this->wrap($partition).' '.$orders); + + return ', row_number() over ('.$over.') as '.$this->wrap('laravel_row'); + } + /** * Compile the "offset" portions of the query. * @@ -1090,6 +1180,21 @@ public function compileInsertUsing(Builder $query, array $columns, string $sql) return "insert into {$table} ({$this->columnize($columns)}) $sql"; } + /** + * Compile an insert ignore statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + * + * @throws \RuntimeException + */ + public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) + { + throw new RuntimeException('This database engine does not support inserting while ignoring errors.'); + } + /** * Compile an update statement into SQL. * @@ -1183,6 +1288,8 @@ public function prepareBindingsForUpdate(array $bindings, array $values) { $cleanBindings = Arr::except($bindings, ['select', 'join']); + $values = Arr::flatten(array_map(fn ($value) => value($value), $values)); + return array_values( array_merge($bindings['join'], $values, Arr::flatten($cleanBindings)) ); diff --git a/src/Illuminate/Database/Query/Grammars/MariaDbGrammar.php b/src/Illuminate/Database/Query/Grammars/MariaDbGrammar.php new file mode 100755 index 000000000000..37d2cc015431 --- /dev/null +++ b/src/Illuminate/Database/Query/Grammars/MariaDbGrammar.php @@ -0,0 +1,19 @@ +useLegacyGroupLimit($query) + ? $this->compileLegacyGroupLimit($query) + : parent::compileGroupLimit($query); + } + + /** + * Determine whether to use a legacy group limit clause for MySQL < 8.0. + * + * @param \Illuminate\Database\Query\Builder $query + * @return bool + */ + public function useLegacyGroupLimit(Builder $query) + { + $version = $query->getConnection()->getServerVersion(); + + return ! $query->getConnection()->isMaria() && version_compare($version, '8.0.11') < 0; + } + + /** + * Compile a group limit clause for MySQL < 8.0. + * + * Derived from https://softonsofa.com/tweaking-eloquent-relations-how-to-get-n-related-models-per-parent/. + * + * @param \Illuminate\Database\Query\Builder $query + * @return string + */ + protected function compileLegacyGroupLimit(Builder $query) + { + $limit = (int) $query->groupLimit['value']; + $offset = $query->offset; + + if (isset($offset)) { + $offset = (int) $offset; + $limit += $offset; + + $query->offset = null; + } + + $column = last(explode('.', $query->groupLimit['column'])); + $column = $this->wrap($column); + + $partition = ', @laravel_row := if(@laravel_group = '.$column.', @laravel_row + 1, 1) as `laravel_row`'; + $partition .= ', @laravel_group := '.$column; + + $orders = (array) $query->orders; + + array_unshift($orders, [ + 'column' => $query->groupLimit['column'], + 'direction' => 'asc', + ]); + + $query->orders = $orders; + + $components = $this->compileComponents($query); + + $sql = $this->concatenate($components); + + $from = '(select @laravel_row := 0, @laravel_group := 0) as `laravel_vars`, ('.$sql.') as `laravel_table`'; + + $sql = 'select `laravel_table`.*'.$partition.' from '.$from.' having `laravel_row` <= '.$limit; + + if (isset($offset)) { + $sql .= ' and `laravel_row` > '.$offset; + } + + return $sql.' order by `laravel_row`'; + } + /** * Compile an insert ignore statement into SQL. * @@ -106,6 +183,19 @@ public function compileInsertOrIgnore(Builder $query, array $values) return Str::replaceFirst('insert', 'insert ignore', $this->compileInsert($query, $values)); } + /** + * Compile an insert ignore statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + */ + public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) + { + return Str::replaceFirst('insert', 'insert ignore', $this->compileInsertUsing($query, $columns, $sql)); + } + /** * Compile a "JSON contains" statement into SQL. * @@ -254,6 +344,18 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar return $sql.$columns; } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + return trim("{$join->type} join lateral {$expression} on true"); + } + /** * Prepare a JSON column being updated using the JSON_SET function. * diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index b1786e5111cc..c22720a05c7c 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -323,6 +324,19 @@ public function compileInsertOrIgnore(Builder $query, array $values) return $this->compileInsert($query, $values).' on conflict do nothing'; } + /** + * Compile an insert ignore statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + */ + public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) + { + return $this->compileInsertUsing($query, $columns, $sql).' on conflict do nothing'; + } + /** * Compile an insert and get ID statement into SQL. * @@ -396,6 +410,18 @@ public function compileUpsert(Builder $query, array $values, array $uniqueBy, ar return $sql.$columns; } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + return trim("{$join->type} join lateral {$expression} on true"); + } + /** * Prepares a JSON column being updated using the JSONB_SET function. * diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index 8bf7d39f6935..0c43e4db8bfd 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -146,6 +146,31 @@ protected function compileJsonLength($column, $operator, $value) return 'json_array_length('.$field.$path.') '.$operator.' '.$value; } + /** + * Compile a "JSON contains" statement into SQL. + * + * @param string $column + * @param mixed $value + * @return string + */ + protected function compileJsonContains($column, $value) + { + [$field, $path] = $this->wrapJsonFieldAndPath($column); + + return 'exists (select 1 from json_each('.$field.$path.') where '.$this->wrap('json_each.value').' is '.$value.')'; + } + + /** + * Prepare the binding for a "JSON contains" statement. + * + * @param mixed $binding + * @return mixed + */ + public function prepareBindingForJsonContains($binding) + { + return $binding; + } + /** * Compile a "JSON contains key" statement into SQL. * @@ -159,6 +184,25 @@ protected function compileJsonContainsKey($column) return 'json_type('.$field.$path.') is not null'; } + /** + * Compile a group limit clause. + * + * @param \Illuminate\Database\Query\Builder $query + * @return string + */ + protected function compileGroupLimit(Builder $query) + { + $version = $query->getConnection()->getServerVersion(); + + if (version_compare($version, '3.25.0') >= 0) { + return parent::compileGroupLimit($query); + } + + $query->groupLimit = null; + + return $this->compileSelect($query); + } + /** * Compile an update statement into SQL. * @@ -187,6 +231,19 @@ public function compileInsertOrIgnore(Builder $query, array $values) return Str::replaceFirst('insert', 'insert or ignore', $this->compileInsert($query, $values)); } + /** + * Compile an insert ignore statement using a subquery into SQL. + * + * @param \Illuminate\Database\Query\Builder $query + * @param array $columns + * @param string $sql + * @return string + */ + public function compileInsertOrIgnoreUsing(Builder $query, array $columns, string $sql) + { + return Str::replaceFirst('insert', 'insert or ignore', $this->compileInsertUsing($query, $columns, $sql)); + } + /** * Compile the columns for an update statement. * diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index f68722a64bce..c084308b74ba 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Query\Grammars; use Illuminate\Database\Query\Builder; +use Illuminate\Database\Query\JoinLateralClause; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -312,6 +313,22 @@ protected function compileLimit(Builder $query, $limit) return ''; } + /** + * Compile a row number clause. + * + * @param string $partition + * @param string $orders + * @return string + */ + protected function compileRowNumber($partition, $orders) + { + if (empty($orders)) { + $orders = 'order by (select 0)'; + } + + return parent::compileRowNumber($partition, $orders); + } + /** * Compile the "offset" portions of the query. * @@ -444,6 +461,20 @@ public function prepareBindingsForUpdate(array $bindings, array $values) ); } + /** + * Compile a "lateral join" clause. + * + * @param \Illuminate\Database\Query\JoinLateralClause $join + * @param string $expression + * @return string + */ + public function compileJoinLateral(JoinLateralClause $join, string $expression): string + { + $type = $join->type == 'left' ? 'outer' : 'cross'; + + return trim("{$type} apply {$expression}"); + } + /** * Compile the SQL statement to define a savepoint. * diff --git a/src/Illuminate/Database/Query/JoinLateralClause.php b/src/Illuminate/Database/Query/JoinLateralClause.php new file mode 100644 index 000000000000..1be31d29626a --- /dev/null +++ b/src/Illuminate/Database/Query/JoinLateralClause.php @@ -0,0 +1,8 @@ +column_name; - }, $results); - } - /** * Process the results of a columns query. * @@ -38,7 +23,7 @@ public function processColumns($results) 'nullable' => $result->nullable === 'YES', 'default' => $result->default, 'auto_increment' => $result->extra === 'auto_increment', - 'comment' => $result->comment, + 'comment' => $result->comment ?: null, ]; }, $results); } diff --git a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index d35ee2dc2c6c..71c3e862ca37 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -30,21 +30,6 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu return is_numeric($id) ? (int) $id : $id; } - /** - * Process the results of a column listing query. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param array $results - * @return array - */ - public function processColumnListing($results) - { - return array_map(function ($result) { - return ((object) $result)->column_name; - }, $results); - } - /** * Process the results of a types query. * @@ -107,7 +92,7 @@ public function processColumns($results) $autoincrement = $result->default !== null && str_starts_with($result->default, 'nextval('); return [ - 'name' => str_starts_with($result->name, '"') ? str_replace('"', '', $result->name) : $result->name, + 'name' => $result->name, 'type_name' => $result->type_name, 'type' => $result->type, 'collation' => $result->collation, diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 97a994ebc221..936e6245b170 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -120,17 +120,4 @@ public function processForeignKeys($results) { return $results; } - - /** - * Process the results of a column listing query. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param array $results - * @return array - */ - public function processColumnListing($results) - { - return $results; - } } diff --git a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php index 8f5fb98206e0..63cc84c067da 100644 --- a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php @@ -4,41 +4,33 @@ class SQLiteProcessor extends Processor { - /** - * Process the results of a column listing query. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param array $results - * @return array - */ - public function processColumnListing($results) - { - return array_map(function ($result) { - return ((object) $result)->name; - }, $results); - } - /** * Process the results of a columns query. * * @param array $results + * @param string $sql * @return array */ - public function processColumns($results) + public function processColumns($results, $sql = '') { $hasPrimaryKey = array_sum(array_column($results, 'primary')) === 1; - return array_map(function ($result) use ($hasPrimaryKey) { + return array_map(function ($result) use ($hasPrimaryKey, $sql) { $result = (object) $result; $type = strtolower($result->type); + $collation = preg_match( + '/\b'.preg_quote($result->name).'\b[^,(]+(?:\([^()]+\)[^,]*)?(?:(?:default|check|as)\s*(?:\(.*?\))?[^,]*)*collate\s+["\'`]?(\w+)/i', + $sql, + $matches + ) === 1 ? strtolower($matches[1]) : null; + return [ 'name' => $result->name, - 'type_name' => strtok($type, '('), + 'type_name' => strtok($type, '(') ?: '', 'type' => $type, - 'collation' => null, + 'collation' => $collation, 'nullable' => (bool) $result->nullable, 'default' => $result->default, 'auto_increment' => $hasPrimaryKey && $result->primary && $type === 'integer', diff --git a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php index c089593ed86a..8c632060b025 100755 --- a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php @@ -55,21 +55,6 @@ protected function processInsertGetIdForOdbc(Connection $connection) return is_object($row) ? $row->insertid : $row['insertid']; } - /** - * Process the results of a column listing query. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param array $results - * @return array - */ - public function processColumnListing($results) - { - return array_map(function ($result) { - return ((object) $result)->name; - }, $results); - } - /** * Process the results of a columns query. * diff --git a/src/Illuminate/Database/SQLiteConnection.php b/src/Illuminate/Database/SQLiteConnection.php index ad7c1486d2d2..536fca3164b9 100755 --- a/src/Illuminate/Database/SQLiteConnection.php +++ b/src/Illuminate/Database/SQLiteConnection.php @@ -3,7 +3,6 @@ namespace Illuminate\Database; use Exception; -use Illuminate\Database\PDO\SQLiteDriver; use Illuminate\Database\Query\Grammars\SQLiteGrammar as QueryGrammar; use Illuminate\Database\Query\Processors\SQLiteProcessor; use Illuminate\Database\Schema\Grammars\SQLiteGrammar as SchemaGrammar; @@ -32,9 +31,17 @@ public function __construct($pdo, $database = '', $tablePrefix = '', array $conf return; } - $enableForeignKeyConstraints - ? $this->getSchemaBuilder()->enableForeignKeyConstraints() - : $this->getSchemaBuilder()->disableForeignKeyConstraints(); + $schemaBuilder = $this->getSchemaBuilder(); + + try { + $enableForeignKeyConstraints + ? $schemaBuilder->enableForeignKeyConstraints() + : $schemaBuilder->disableForeignKeyConstraints(); + } catch (QueryException $e) { + if (! $e->getPrevious() instanceof SQLiteDatabaseDoesNotExistException) { + throw $e; + } + } } /** @@ -122,16 +129,6 @@ protected function getDefaultPostProcessor() return new SQLiteProcessor; } - /** - * Get the Doctrine DBAL driver. - * - * @return \Illuminate\Database\PDO\SQLiteDriver - */ - protected function getDoctrineDriver() - { - return new SQLiteDriver; - } - /** * Get the database connection foreign key constraints configuration option. * diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 640cf0177c37..cd254b71b3f2 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -2,13 +2,12 @@ namespace Illuminate\Database\Schema; -use BadMethodCallException; use Closure; use Illuminate\Database\Connection; use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Grammars\Grammar; -use Illuminate\Database\SQLiteConnection; +use Illuminate\Database\Schema\Grammars\MySqlGrammar; use Illuminate\Support\Fluent; use Illuminate\Support\Traits\Macroable; @@ -130,6 +129,10 @@ public function toSql(Connection $connection, Grammar $grammar) $this->ensureCommandsAreValid($connection); foreach ($this->commands as $command) { + if ($command->shouldBeSkipped) { + continue; + } + $method = 'compile'.ucfirst($command->name); if (method_exists($grammar, $method) || $grammar::hasMacro($method)) { @@ -152,20 +155,7 @@ public function toSql(Connection $connection, Grammar $grammar) */ protected function ensureCommandsAreValid(Connection $connection) { - if ($connection instanceof SQLiteConnection) { - if ($this->commandsNamed(['dropColumn', 'renameColumn'])->count() > 1 - && ! $connection->usingNativeSchemaOperations()) { - throw new BadMethodCallException( - "SQLite doesn't support multiple calls to dropColumn / renameColumn in a single modification." - ); - } - - if ($this->commandsNamed(['dropForeign'])->count() > 0) { - throw new BadMethodCallException( - "SQLite doesn't support dropping foreign keys (you would need to re-create the table)." - ); - } - } + // } /** @@ -198,7 +188,7 @@ protected function addImpliedCommands(Connection $connection, Grammar $grammar) array_unshift($this->commands, $this->createCommand('change')); } - $this->addFluentIndexes(); + $this->addFluentIndexes($connection, $grammar); $this->addFluentCommands($connection, $grammar); } @@ -206,12 +196,21 @@ protected function addImpliedCommands(Connection $connection, Grammar $grammar) /** * Add the index commands fluently specified on columns. * + * @param \Illuminate\Database\Connection $connection + * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar * @return void */ - protected function addFluentIndexes() + protected function addFluentIndexes(Connection $connection, Grammar $grammar) { foreach ($this->columns as $column) { foreach (['primary', 'unique', 'index', 'fulltext', 'fullText', 'spatialIndex'] as $index) { + // If the column is supposed to be changed to an auto increment column and + // the specified index is primary, there is no need to add a command on + // MySQL, as it will be handled during the column definition instead. + if ($index === 'primary' && $column->autoIncrement && $column->change && $grammar instanceof MySqlGrammar) { + continue 2; + } + // If the index has been specified on the given column, but is simply equal // to "true" (boolean), no name has been specified for this index so the // index method can be called without a name and it will generate one. @@ -255,10 +254,6 @@ protected function addFluentIndexes() public function addFluentCommands(Connection $connection, Grammar $grammar) { foreach ($this->columns as $column) { - if ($column->change && ! $connection->usingNativeSchemaOperations()) { - continue; - } - foreach ($grammar->getFluentCommands() as $commandName) { $this->addCommand($commandName, compact('column')); } @@ -309,6 +304,28 @@ public function innoDb() $this->engine('InnoDB'); } + /** + * Specify the character set that should be used for the table. + * + * @param string $charset + * @return void + */ + public function charset($charset) + { + $this->charset = $charset; + } + + /** + * Specify the collation that should be used for the table. + * + * @param string $collation + * @return void + */ + public function collation($collation) + { + $this->collation = $collation; + } + /** * Indicate that the table needs to be temporary. * @@ -1341,100 +1358,26 @@ public function macAddress($column = 'mac_address') * Create a new geometry column on the table. * * @param string $column + * @param string|null $subtype + * @param int $srid * @return \Illuminate\Database\Schema\ColumnDefinition */ - public function geometry($column) - { - return $this->addColumn('geometry', $column); - } - - /** - * Create a new point column on the table. - * - * @param string $column - * @param int|null $srid - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function point($column, $srid = null) - { - return $this->addColumn('point', $column, compact('srid')); - } - - /** - * Create a new linestring column on the table. - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function lineString($column) - { - return $this->addColumn('linestring', $column); - } - - /** - * Create a new polygon column on the table. - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function polygon($column) - { - return $this->addColumn('polygon', $column); - } - - /** - * Create a new geometrycollection column on the table. - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function geometryCollection($column) + public function geometry($column, $subtype = null, $srid = 0) { - return $this->addColumn('geometrycollection', $column); + return $this->addColumn('geometry', $column, compact('subtype', 'srid')); } /** - * Create a new multipoint column on the table. + * Create a new geography column on the table. * * @param string $column + * @param string|null $subtype + * @param int $srid * @return \Illuminate\Database\Schema\ColumnDefinition */ - public function multiPoint($column) + public function geography($column, $subtype = null, $srid = 4326) { - return $this->addColumn('multipoint', $column); - } - - /** - * Create a new multilinestring column on the table. - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function multiLineString($column) - { - return $this->addColumn('multilinestring', $column); - } - - /** - * Create a new multipolygon column on the table. - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function multiPolygon($column) - { - return $this->addColumn('multipolygon', $column); - } - - /** - * Create a new multipolygon column on the table. - * - * @param string $column - * @return \Illuminate\Database\Schema\ColumnDefinition - */ - public function multiPolygonZ($column) - { - return $this->addColumn('multipolygonz', $column); + return $this->addColumn('geography', $column, compact('subtype', 'srid')); } /** @@ -1656,7 +1599,11 @@ protected function dropIndexCommand($command, $type, $index) */ protected function createIndexName($type, array $columns) { - $index = strtolower($this->prefix.$this->table.'_'.implode('_', $columns).'_'.$type); + $table = str_contains($this->table, '.') + ? substr_replace($this->table, '.'.$this->prefix, strrpos($this->table, '.'), 1) + : $this->prefix.$this->table; + + $index = strtolower($table.'_'.implode('_', $columns).'_'.$type); return str_replace(['-', '.'], '_', $index); } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 94d655c0f1ca..808b791b66af 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -5,11 +5,14 @@ use Closure; use Illuminate\Container\Container; use Illuminate\Database\Connection; +use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; use LogicException; class Builder { + use Macroable; + /** * The database connection instance. * @@ -45,13 +48,6 @@ class Builder */ public static $defaultMorphKeyType = 'int'; - /** - * Indicates whether Doctrine DBAL usage will be prevented if possible when dropping, renaming, and modifying columns. - * - * @var bool - */ - public static $alwaysUsesNativeSchemaOperationsIfPossible = false; - /** * Create a new database Schema manager. * @@ -112,17 +108,6 @@ public static function morphUsingUlids() return static::defaultMorphKeyType('ulid'); } - /** - * Attempt to use native schema operations for dropping, renaming, and modifying columns, even if Doctrine DBAL is installed. - * - * @param bool $value - * @return void - */ - public static function useNativeSchemaOperationsIfPossible(bool $value = true) - { - static::$alwaysUsesNativeSchemaOperationsIfPossible = $value; - } - /** * Create a database in the schema. * @@ -199,6 +184,16 @@ public function getTables() ); } + /** + * Get the names of the tables that belong to the database. + * + * @return array + */ + public function getTableListing() + { + return array_column($this->getTables(), 'name'); + } + /** * Get the views that belong to the database. * @@ -221,20 +216,6 @@ public function getTypes() throw new LogicException('This database driver does not support user-defined types.'); } - /** - * Get all of the table names for the database. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return array - * - * @throws \LogicException - */ - public function getAllTables() - { - throw new LogicException('This database driver does not support getting all tables.'); - } - /** * Determine if the given table has a given column. * @@ -309,14 +290,6 @@ public function whenTableDoesntHaveColumn(string $table, string $column, Closure */ public function getColumnType($table, $column, $fullDefinition = false) { - if (! $this->connection->usingNativeSchemaOperations()) { - $table = $this->connection->getTablePrefix().$table; - - $type = $this->connection->getDoctrineColumn($table, $column)->getType(); - - return $type::lookupName($type); - } - $columns = $this->getColumns($table); foreach ($columns as $value) { @@ -369,6 +342,43 @@ public function getIndexes($table) ); } + /** + * Get the names of the indexes for a given table. + * + * @param string $table + * @return array + */ + public function getIndexListing($table) + { + return array_column($this->getIndexes($table), 'name'); + } + + /** + * Determine if the given table has a given index. + * + * @param string $table + * @param string|array $index + * @param string|null $type + * @return bool + */ + public function hasIndex($table, $index, $type = null) + { + $type = is_null($type) ? $type : strtolower($type); + + foreach ($this->getIndexes($table) as $value) { + $typeMatches = is_null($type) + || ($type === 'primary' && $value['primary']) + || ($type === 'unique' && $value['unique']) + || $type === $value['type']; + + if (($value['name'] === $index || $value['columns'] === $index) && $typeMatches) { + return true; + } + } + + return false; + } + /** * Get the foreign keys for a given table. * diff --git a/src/Illuminate/Database/Schema/ColumnDefinition.php b/src/Illuminate/Database/Schema/ColumnDefinition.php index 51265ac4213e..0093ee1f25c6 100644 --- a/src/Illuminate/Database/Schema/ColumnDefinition.php +++ b/src/Illuminate/Database/Schema/ColumnDefinition.php @@ -10,23 +10,23 @@ * @method $this autoIncrement() Set INTEGER columns as auto-increment (primary key) * @method $this change() Change the column * @method $this charset(string $charset) Specify a character set for the column (MySQL) - * @method $this collation(string $collation) Specify a collation for the column (MySQL/PostgreSQL/SQL Server) + * @method $this collation(string $collation) Specify a collation for the column * @method $this comment(string $comment) Add a comment to the column (MySQL/PostgreSQL) * @method $this default(mixed $value) Specify a "default" value for the column * @method $this first() Place the column "first" in the table (MySQL) * @method $this from(int $startingValue) Set the starting value of an auto-incrementing field (MySQL / PostgreSQL) - * @method $this generatedAs(string|Expression $expression = null) Create a SQL compliant identity column (PostgreSQL) - * @method $this index(string $indexName = null) Add an index + * @method $this generatedAs(string|\Illuminate\Database\Query\Expression $expression = null) Create a SQL compliant identity column (PostgreSQL) + * @method $this index(bool|string $indexName = null) Add an index * @method $this invisible() Specify that the column should be invisible to "SELECT *" (MySQL) * @method $this nullable(bool $value = true) Allow NULL values to be inserted into the column * @method $this persisted() Mark the computed generated column as persistent (SQL Server) - * @method $this primary() Add a primary index - * @method $this fulltext(string $indexName = null) Add a fulltext index - * @method $this spatialIndex(string $indexName = null) Add a spatial index + * @method $this primary(bool $value = true) Add a primary index + * @method $this fulltext(bool|string $indexName = null) Add a fulltext index + * @method $this spatialIndex(bool|string $indexName = null) Add a spatial index * @method $this startingValue(int $startingValue) Set the starting value of an auto-incrementing field (MySQL/PostgreSQL) * @method $this storedAs(string $expression) Create a stored generated column (MySQL/PostgreSQL/SQLite) * @method $this type(string $type) Specify a type for the column - * @method $this unique(string $indexName = null) Add a unique index + * @method $this unique(bool|string $indexName = null) Add a unique index * @method $this unsigned() Set the INTEGER column as UNSIGNED (MySQL) * @method $this useCurrent() Set the TIMESTAMP column to use CURRENT_TIMESTAMP as default value * @method $this useCurrentOnUpdate() Set the TIMESTAMP column to use CURRENT_TIMESTAMP when updating (MySQL) diff --git a/src/Illuminate/Database/Schema/Grammars/ChangeColumn.php b/src/Illuminate/Database/Schema/Grammars/ChangeColumn.php deleted file mode 100644 index 7f429c0eccc5..000000000000 --- a/src/Illuminate/Database/Schema/Grammars/ChangeColumn.php +++ /dev/null @@ -1,238 +0,0 @@ -isDoctrineAvailable()) { - throw new RuntimeException(sprintf( - 'Changing columns for table "%s" requires Doctrine DBAL. Please install the doctrine/dbal package.', - $blueprint->getTable() - )); - } - - $schema = $connection->getDoctrineSchemaManager(); - $databasePlatform = $connection->getDoctrineConnection()->getDatabasePlatform(); - $databasePlatform->registerDoctrineTypeMapping('enum', 'string'); - - $tableDiff = static::getChangedDiff( - $grammar, $blueprint, $schema - ); - - if (! $tableDiff->isEmpty()) { - return (array) $databasePlatform->getAlterTableSQL($tableDiff); - } - - return []; - } - - /** - * Get the Doctrine table difference for the given changes. - * - * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema - * @return \Doctrine\DBAL\Schema\TableDiff - */ - protected static function getChangedDiff($grammar, Blueprint $blueprint, SchemaManager $schema) - { - $current = $schema->introspectTable($grammar->getTablePrefix().$blueprint->getTable()); - - return $schema->createComparator()->compareTables( - $current, static::getTableWithColumnChanges($blueprint, $current) - ); - } - - /** - * Get a copy of the given Doctrine table after making the column changes. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Doctrine\DBAL\Schema\Table $table - * @return \Doctrine\DBAL\Schema\Table - */ - protected static function getTableWithColumnChanges(Blueprint $blueprint, Table $table) - { - $table = clone $table; - - foreach ($blueprint->getChangedColumns() as $fluent) { - $column = static::getDoctrineColumn($table, $fluent); - - // Here we will spin through each fluent column definition and map it to the proper - // Doctrine column definitions - which is necessary because Laravel and Doctrine - // use some different terminology for various column attributes on the tables. - foreach ($fluent->getAttributes() as $key => $value) { - if (! is_null($option = static::mapFluentOptionToDoctrine($key))) { - if (method_exists($column, $method = 'set'.ucfirst($option))) { - $column->{$method}(static::mapFluentValueToDoctrine($option, $value)); - continue; - } - - $column->setPlatformOption($option, static::mapFluentValueToDoctrine($option, $value)); - } - } - } - - return $table; - } - - /** - * Get the Doctrine column instance for a column change. - * - * @param \Doctrine\DBAL\Schema\Table $table - * @param \Illuminate\Support\Fluent $fluent - * @return \Doctrine\DBAL\Schema\Column - */ - protected static function getDoctrineColumn(Table $table, Fluent $fluent) - { - return $table->modifyColumn( - $fluent['name'], static::getDoctrineColumnChangeOptions($fluent) - )->getColumn($fluent['name']); - } - - /** - * Get the Doctrine column change options. - * - * @param \Illuminate\Support\Fluent $fluent - * @return array - */ - protected static function getDoctrineColumnChangeOptions(Fluent $fluent) - { - $options = ['Type' => static::getDoctrineColumnType($fluent['type'])]; - - if (! in_array($fluent['type'], ['smallint', 'integer', 'bigint'])) { - $options['Autoincrement'] = false; - } - - if (in_array($fluent['type'], ['tinyText', 'text', 'mediumText', 'longText'])) { - $options['Length'] = static::calculateDoctrineTextLength($fluent['type']); - } - - if ($fluent['type'] === 'char') { - $options['Fixed'] = true; - } - - if (static::doesntNeedCharacterOptions($fluent['type'])) { - $options['PlatformOptions'] = [ - 'collation' => '', - 'charset' => '', - ]; - } - - return $options; - } - - /** - * Get the doctrine column type. - * - * @param string $type - * @return \Doctrine\DBAL\Types\Type - */ - protected static function getDoctrineColumnType($type) - { - $type = strtolower($type); - - return Type::getType(match ($type) { - 'biginteger' => 'bigint', - 'smallinteger' => 'smallint', - 'tinytext', 'mediumtext', 'longtext' => 'text', - 'binary' => 'blob', - 'uuid' => 'guid', - 'char' => 'string', - 'double' => 'float', - default => $type, - }); - } - - /** - * Calculate the proper column length to force the Doctrine text type. - * - * @param string $type - * @return int - */ - protected static function calculateDoctrineTextLength($type) - { - return match ($type) { - 'tinyText' => 1, - 'mediumText' => 65535 + 1, - 'longText' => 16777215 + 1, - default => 255 + 1, - }; - } - - /** - * Determine if the given type does not need character / collation options. - * - * @param string $type - * @return bool - */ - protected static function doesntNeedCharacterOptions($type) - { - return in_array($type, [ - 'bigInteger', - 'binary', - 'boolean', - 'date', - 'dateTime', - 'decimal', - 'double', - 'float', - 'integer', - 'json', - 'mediumInteger', - 'smallInteger', - 'time', - 'timestamp', - 'tinyInteger', - ]); - } - - /** - * Get the matching Doctrine option for a given Fluent attribute name. - * - * @param string $attribute - * @return string|null - */ - protected static function mapFluentOptionToDoctrine($attribute) - { - return match ($attribute) { - 'type', 'name' => null, - 'nullable' => 'notnull', - 'total' => 'precision', - 'places' => 'scale', - default => $attribute, - }; - } - - /** - * Get the matching Doctrine value for a given Fluent attribute. - * - * @param string $option - * @param mixed $value - * @return mixed - */ - protected static function mapFluentValueToDoctrine($option, $value) - { - return $option === 'notnull' ? ! $value : $value; - } -} diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index 9933a38be0fc..86f4290b39d8 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -3,8 +3,6 @@ namespace Illuminate\Database\Schema\Grammars; use BackedEnum; -use Doctrine\DBAL\Schema\AbstractSchemaManager as SchemaManager; -use Doctrine\DBAL\Schema\TableDiff; use Illuminate\Contracts\Database\Query\Expression; use Illuminate\Database\Concerns\CompilesJsonPaths; use Illuminate\Database\Connection; @@ -76,7 +74,11 @@ public function compileDropDatabaseIfExists($name) */ public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection) { - return RenameColumn::compile($this, $blueprint, $command, $connection); + return sprintf('alter table %s rename column %s to %s', + $this->wrapTable($blueprint), + $this->wrap($command->from), + $this->wrap($command->to) + ); } /** @@ -91,7 +93,7 @@ public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Conne */ public function compileChange(Blueprint $blueprint, Fluent $command, Connection $connection) { - return ChangeColumn::compile($this, $blueprint, $command, $connection); + throw new LogicException('This database driver does not support modifying columns.'); } /** @@ -162,6 +164,18 @@ public function compileForeign(Blueprint $blueprint, Fluent $command) return $sql; } + /** + * Compile a drop foreign key command. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $command + * @return string + */ + public function compileDropForeign(Blueprint $blueprint, Fluent $command) + { + throw new RuntimeException('This database driver does not support dropping foreign keys.'); + } + /** * Compile the blueprint's added column definitions. * @@ -228,7 +242,7 @@ protected function addModifiers($sql, Blueprint $blueprint, Fluent $column) } /** - * Get the primary key command if it exists on the blueprint. + * Get the command with a given name if it exists on the blueprint. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param string $name @@ -257,6 +271,24 @@ protected function getCommandsByName(Blueprint $blueprint, $name) }); } + /* + * Determine if a command with a given name exists on the blueprint. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param string $name + * @return bool + */ + protected function hasCommand(Blueprint $blueprint, $name) + { + foreach ($blueprint->getCommands() as $command) { + if ($command->name === $name) { + return true; + } + } + + return false; + } + /** * Add a prefix to an array of values. * @@ -319,22 +351,6 @@ protected function getDefaultValue($value) : "'".(string) $value."'"; } - /** - * Create an empty Doctrine DBAL TableDiff from the Blueprint. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema - * @return \Doctrine\DBAL\Schema\TableDiff - */ - public function getDoctrineTableDiff(Blueprint $blueprint, SchemaManager $schema) - { - $tableName = $this->getTablePrefix().$blueprint->getTable(); - - $table = $schema->introspectTable($tableName); - - return $schema->createComparator()->compareTables(oldTable: $table, newTable: $table); - } - /** * Get the fluent commands for the grammar. * diff --git a/src/Illuminate/Database/Schema/Grammars/MariaDbGrammar.php b/src/Illuminate/Database/Schema/Grammars/MariaDbGrammar.php new file mode 100755 index 000000000000..39ae68619127 --- /dev/null +++ b/src/Illuminate/Database/Schema/Grammars/MariaDbGrammar.php @@ -0,0 +1,87 @@ +getServerVersion(), '10.5.2', '<')) { + $column = collect($connection->getSchemaBuilder()->getColumns($blueprint->getTable())) + ->firstWhere('name', $command->from); + + $modifiers = $this->addModifiers($column['type'], $blueprint, new ColumnDefinition([ + 'change' => true, + 'type' => match ($column['type_name']) { + 'bigint' => 'bigInteger', + 'int' => 'integer', + 'mediumint' => 'mediumInteger', + 'smallint' => 'smallInteger', + 'tinyint' => 'tinyInteger', + default => $column['type_name'], + }, + 'nullable' => $column['nullable'], + 'default' => $column['default'] && str_starts_with(strtolower($column['default']), 'current_timestamp') + ? new Expression($column['default']) + : $column['default'], + 'autoIncrement' => $column['auto_increment'], + 'collation' => $column['collation'], + 'comment' => $column['comment'], + ])); + + return sprintf('alter table %s change %s %s %s', + $this->wrapTable($blueprint), + $this->wrap($command->from), + $this->wrap($command->to), + $modifiers + ); + } + + return parent::compileRenameColumn($blueprint, $command, $connection); + } + + /** + * Create the column definition for a uuid type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeUuid(Fluent $column) + { + return 'uuid'; + } + + /** + * Create the column definition for a spatial Geometry type. + * + * @param \Illuminate\Support\Fluent $column + * @return string + */ + protected function typeGeometry(Fluent $column) + { + $subtype = $column->subtype ? strtolower($column->subtype) : null; + + if (! in_array($subtype, ['point', 'linestring', 'polygon', 'geometrycollection', 'multipoint', 'multilinestring', 'multipolygon'])) { + $subtype = null; + } + + return sprintf('%s%s', + $subtype ?? 'geometry', + $column->srid ? ' ref_system_id='.$column->srid : '' + ); + } +} diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 07847fa23f8e..aec73db6eef4 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -5,6 +5,7 @@ use Illuminate\Database\Connection; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\ColumnDefinition; use Illuminate\Support\Fluent; use RuntimeException; @@ -17,7 +18,7 @@ class MySqlGrammar extends Grammar */ protected $modifiers = [ 'Unsigned', 'Charset', 'Collate', 'VirtualAs', 'StoredAs', 'Nullable', - 'Srid', 'Default', 'OnUpdate', 'Invisible', 'Increment', 'Comment', 'After', 'First', + 'Default', 'OnUpdate', 'Invisible', 'Increment', 'Comment', 'After', 'First', ]; /** @@ -43,11 +44,21 @@ class MySqlGrammar extends Grammar */ public function compileCreateDatabase($name, $connection) { + $charset = $connection->getConfig('charset'); + $collation = $connection->getConfig('collation'); + + if (! $charset || ! $collation) { + return sprintf( + 'create database %s', + $this->wrapValue($name), + ); + } + return sprintf( 'create database %s default character set %s default collate %s', $this->wrapValue($name), - $this->wrapValue($connection->getConfig('charset')), - $this->wrapValue($connection->getConfig('collation')), + $this->wrapValue($charset), + $this->wrapValue($collation), ); } @@ -65,18 +76,6 @@ public function compileDropDatabaseIfExists($name) ); } - /** - * Compile the query to determine the list of tables. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileTableExists() - { - return "select * from information_schema.tables where table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"; - } - /** * Compile the query to determine the tables. * @@ -88,7 +87,7 @@ public function compileTables($database) return sprintf( 'select table_name as `name`, (data_length + index_length) as `size`, ' .'table_comment as `comment`, engine as `engine`, table_collation as `collation` ' - ."from information_schema.tables where table_schema = %s and table_type = 'BASE TABLE' " + ."from information_schema.tables where table_schema = %s and table_type in ('BASE TABLE', 'SYSTEM VERSIONED') " .'order by table_name', $this->quoteString($database) ); @@ -110,42 +109,6 @@ public function compileViews($database) ); } - /** - * Compile the SQL needed to retrieve all table names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileGetAllTables() - { - return 'SHOW FULL TABLES WHERE table_type = \'BASE TABLE\''; - } - - /** - * Compile the SQL needed to retrieve all view names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileGetAllViews() - { - return 'SHOW FULL TABLES WHERE table_type = \'VIEW\''; - } - - /** - * Compile the query to determine the list of columns. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileColumnListing() - { - return 'select column_name as `column_name` from information_schema.columns where table_schema = ? and table_name = ?'; - } - /** * Compile the query to determine the columns. * @@ -158,7 +121,7 @@ public function compileColumns($database, $table) return sprintf( 'select column_name as `name`, data_type as `type_name`, column_type as `type`, ' .'collation_name as `collation`, is_nullable as `nullable`, ' - .'column_default as `default`, column_comment AS `comment`, extra as `extra` ' + .'column_default as `default`, column_comment as `comment`, extra as `extra` ' .'from information_schema.columns where table_schema = %s and table_name = %s ' .'order by ordinal_position asc', $this->quoteString($database), @@ -248,10 +211,22 @@ public function compileCreate(Blueprint $blueprint, Fluent $command, Connection */ protected function compileCreateTable($blueprint, $command, $connection) { + $tableStructure = $this->getColumns($blueprint); + + if ($primaryKey = $this->getCommandByName($blueprint, 'primary')) { + $tableStructure[] = sprintf( + 'primary key %s(%s)', + $primaryKey->algorithm ? 'using '.$primaryKey->algorithm : '', + $this->columnize($primaryKey->columns) + ); + + $primaryKey->shouldBeSkipped = true; + } + return sprintf('%s table %s (%s)', $blueprint->temporary ? 'create temporary' : 'create', $this->wrapTable($blueprint), - implode(', ', $this->getColumns($blueprint)) + implode(', ', $tableStructure) ); } @@ -344,13 +319,41 @@ public function compileAutoIncrementStartingValues(Blueprint $blueprint, Fluent */ public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection) { - return $connection->usingNativeSchemaOperations() - ? sprintf('alter table %s rename column %s to %s', + $version = $connection->getServerVersion(); + + if (($connection->isMaria() && version_compare($version, '10.5.2', '<')) || + (! $connection->isMaria() && version_compare($version, '8.0.3', '<'))) { + $column = collect($connection->getSchemaBuilder()->getColumns($blueprint->getTable())) + ->firstWhere('name', $command->from); + + $modifiers = $this->addModifiers($column['type'], $blueprint, new ColumnDefinition([ + 'change' => true, + 'type' => match ($column['type_name']) { + 'bigint' => 'bigInteger', + 'int' => 'integer', + 'mediumint' => 'mediumInteger', + 'smallint' => 'smallInteger', + 'tinyint' => 'tinyInteger', + default => $column['type_name'], + }, + 'nullable' => $column['nullable'], + 'default' => $column['default'] && str_starts_with(strtolower($column['default']), 'current_timestamp') + ? new Expression($column['default']) + : $column['default'], + 'autoIncrement' => $column['auto_increment'], + 'collation' => $column['collation'], + 'comment' => $column['comment'], + ])); + + return sprintf('alter table %s change %s %s %s', $this->wrapTable($blueprint), $this->wrap($command->from), - $this->wrap($command->to) - ) - : parent::compileRenameColumn($blueprint, $command, $connection); + $this->wrap($command->to), + $modifiers + ); + } + + return parent::compileRenameColumn($blueprint, $command, $connection); } /** @@ -365,10 +368,6 @@ public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Conne */ public function compileChange(Blueprint $blueprint, Fluent $command, Connection $connection) { - if (! $connection->usingNativeSchemaOperations()) { - return parent::compileChange($blueprint, $command, $connection); - } - $columns = []; foreach ($blueprint->getChangedColumns() as $column) { @@ -1042,86 +1041,33 @@ protected function typeMacAddress(Fluent $column) * @param \Illuminate\Support\Fluent $column * @return string */ - public function typeGeometry(Fluent $column) + protected function typeGeometry(Fluent $column) { - return 'geometry'; - } + $subtype = $column->subtype ? strtolower($column->subtype) : null; - /** - * Create the column definition for a spatial Point type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typePoint(Fluent $column) - { - return 'point'; - } - - /** - * Create the column definition for a spatial LineString type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeLineString(Fluent $column) - { - return 'linestring'; - } - - /** - * Create the column definition for a spatial Polygon type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typePolygon(Fluent $column) - { - return 'polygon'; - } - - /** - * Create the column definition for a spatial GeometryCollection type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeGeometryCollection(Fluent $column) - { - return 'geometrycollection'; - } - - /** - * Create the column definition for a spatial MultiPoint type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeMultiPoint(Fluent $column) - { - return 'multipoint'; - } + if (! in_array($subtype, ['point', 'linestring', 'polygon', 'geometrycollection', 'multipoint', 'multilinestring', 'multipolygon'])) { + $subtype = null; + } - /** - * Create the column definition for a spatial MultiLineString type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeMultiLineString(Fluent $column) - { - return 'multilinestring'; + return sprintf('%s%s', + $subtype ?? 'geometry', + match (true) { + $column->srid && $this->connection?->isMaria() => ' ref_system_id='.$column->srid, + (bool) $column->srid => ' srid '.$column->srid, + default => '', + } + ); } /** - * Create the column definition for a spatial MultiPolygon type. + * Create the column definition for a spatial Geography type. * * @param \Illuminate\Support\Fluent $column * @return string */ - public function typeMultiPolygon(Fluent $column) + protected function typeGeography(Fluent $column) { - return 'multipolygon'; + return $this->typeGeometry($column); } /** @@ -1296,7 +1242,9 @@ protected function modifyOnUpdate(Blueprint $blueprint, Fluent $column) protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { if (in_array($column->type, $this->serials) && $column->autoIncrement) { - return ' auto_increment primary key'; + return $this->hasCommand($blueprint, 'primary') || ($column->change && ! $column->primary) + ? ' auto_increment' + : ' auto_increment primary key'; } } @@ -1342,20 +1290,6 @@ protected function modifyComment(Blueprint $blueprint, Fluent $column) } } - /** - * Get the SQL for a SRID column modifier. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $column - * @return string|null - */ - protected function modifySrid(Blueprint $blueprint, Fluent $column) - { - if (is_int($column->srid) && $column->srid > 0) { - return ' srid '.$column->srid; - } - } - /** * Wrap a single string in keyword identifiers. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index a0b67f01ca60..aad87542d552 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -68,18 +68,6 @@ public function compileDropDatabaseIfExists($name) ); } - /** - * Compile the query to determine if a table exists. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileTableExists() - { - return "select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = 'BASE TABLE'"; - } - /** * Compile the query to determine the tables. * @@ -89,7 +77,7 @@ public function compileTables() { return 'select c.relname as name, n.nspname as schema, pg_total_relation_size(c.oid) as size, ' ."obj_description(c.oid, 'pg_class') as comment from pg_class c, pg_namespace n " - ."where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema')" + ."where c.relkind in ('r', 'p') and n.oid = c.relnamespace and n.nspname not in ('pg_catalog', 'information_schema') " .'order by c.relname'; } @@ -121,44 +109,6 @@ public function compileTypes() ."and n.nspname not in ('pg_catalog', 'information_schema')"; } - /** - * Compile the SQL needed to retrieve all table names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param string|array $searchPath - * @return string - */ - public function compileGetAllTables($searchPath) - { - return "select tablename, concat('\"', schemaname, '\".\"', tablename, '\"') as qualifiedname from pg_catalog.pg_tables where schemaname in ('".implode("','", (array) $searchPath)."')"; - } - - /** - * Compile the SQL needed to retrieve all view names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param string|array $searchPath - * @return string - */ - public function compileGetAllViews($searchPath) - { - return "select viewname, concat('\"', schemaname, '\".\"', viewname, '\"') as qualifiedname from pg_catalog.pg_views where schemaname in ('".implode("','", (array) $searchPath)."')"; - } - - /** - * Compile the query to determine the list of columns. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileColumnListing() - { - return 'select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?'; - } - /** * Compile the query to determine the columns. * @@ -169,11 +119,11 @@ public function compileColumnListing() public function compileColumns($schema, $table) { return sprintf( - 'select quote_ident(a.attname) as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, ' + 'select a.attname as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, ' .'(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, ' .'not a.attnotnull as nullable, ' .'(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, ' - .'(select pg_description.description from pg_description where pg_description.objoid = c.oid and a.attnum = pg_description.objsubid) as comment ' + .'col_description(c.oid, a.attnum) as comment ' .'from pg_attribute a, pg_class c, pg_type t, pg_namespace n ' .'where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace ' .'order by a.attnum', @@ -280,27 +230,10 @@ public function compileAutoIncrementStartingValues(Blueprint $blueprint, Fluent { if ($command->column->autoIncrement && $value = $command->column->get('startingValue', $command->column->get('from'))) { - return 'alter sequence '.$blueprint->getTable().'_'.$command->column->name.'_seq restart with '.$value; - } - } + $table = last(explode('.', $blueprint->getTable())); - /** - * Compile a rename column command. - * - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @param \Illuminate\Database\Connection $connection - * @return array|string - */ - public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection) - { - return $connection->usingNativeSchemaOperations() - ? sprintf('alter table %s rename column %s to %s', - $this->wrapTable($blueprint), - $this->wrap($command->from), - $this->wrap($command->to) - ) - : parent::compileRenameColumn($blueprint, $command, $connection); + return 'alter sequence '.$blueprint->getPrefix().$table.'_'.$command->column->name.'_seq restart with '.$value; + } } /** @@ -315,10 +248,6 @@ public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Conne */ public function compileChange(Blueprint $blueprint, Fluent $command, Connection $connection) { - if (! $connection->usingNativeSchemaOperations()) { - return parent::compileChange($blueprint, $command, $connection); - } - $columns = []; foreach ($blueprint->getChangedColumns() as $column) { @@ -533,18 +462,6 @@ public function compileDropAllDomains($domains) return 'drop domain '.implode(',', $this->escapeNames($domains)).' cascade'; } - /** - * Compile the SQL needed to retrieve all type names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileGetAllTypes() - { - return 'select distinct pg_type.typname from pg_type inner join pg_enum on pg_enum.enumtypid = pg_type.oid'; - } - /** * Compile a drop column command. * @@ -568,7 +485,8 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command) */ public function compileDropPrimary(Blueprint $blueprint, Fluent $command) { - $index = $this->wrap("{$blueprint->getPrefix()}{$blueprint->getTable()}_pkey"); + $table = last(explode('.', $blueprint->getTable())); + $index = $this->wrap("{$blueprint->getPrefix()}{$table}_pkey"); return 'alter table '.$this->wrapTable($blueprint)." drop constraint {$index}"; } @@ -816,7 +734,7 @@ protected function typeLongText(Fluent $column) */ protected function typeInteger(Fluent $column) { - return $column->autoIncrement && is_null($column->generatedAs) ? 'serial' : 'integer'; + return $column->autoIncrement && is_null($column->generatedAs) && ! $column->change ? 'serial' : 'integer'; } /** @@ -827,7 +745,7 @@ protected function typeInteger(Fluent $column) */ protected function typeBigInteger(Fluent $column) { - return $column->autoIncrement && is_null($column->generatedAs) ? 'bigserial' : 'bigint'; + return $column->autoIncrement && is_null($column->generatedAs) && ! $column->change ? 'bigserial' : 'bigint'; } /** @@ -860,7 +778,7 @@ protected function typeTinyInteger(Fluent $column) */ protected function typeSmallInteger(Fluent $column) { - return $column->autoIncrement && is_null($column->generatedAs) ? 'smallserial' : 'smallint'; + return $column->autoIncrement && is_null($column->generatedAs) && ! $column->change ? 'smallserial' : 'smallint'; } /** @@ -1107,115 +1025,32 @@ protected function typeMacAddress(Fluent $column) */ protected function typeGeometry(Fluent $column) { - return $this->formatPostGisType('geometry', $column); - } - - /** - * Create the column definition for a spatial Point type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typePoint(Fluent $column) - { - return $this->formatPostGisType('point', $column); - } - - /** - * Create the column definition for a spatial LineString type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeLineString(Fluent $column) - { - return $this->formatPostGisType('linestring', $column); - } - - /** - * Create the column definition for a spatial Polygon type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typePolygon(Fluent $column) - { - return $this->formatPostGisType('polygon', $column); - } - - /** - * Create the column definition for a spatial GeometryCollection type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeGeometryCollection(Fluent $column) - { - return $this->formatPostGisType('geometrycollection', $column); - } - - /** - * Create the column definition for a spatial MultiPoint type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeMultiPoint(Fluent $column) - { - return $this->formatPostGisType('multipoint', $column); - } - - /** - * Create the column definition for a spatial MultiLineString type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeMultiLineString(Fluent $column) - { - return $this->formatPostGisType('multilinestring', $column); - } - - /** - * Create the column definition for a spatial MultiPolygon type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeMultiPolygon(Fluent $column) - { - return $this->formatPostGisType('multipolygon', $column); - } + if ($column->subtype) { + return sprintf('geometry(%s%s)', + strtolower($column->subtype), + $column->srid ? ','.$column->srid : '' + ); + } - /** - * Create the column definition for a spatial MultiPolygonZ type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - protected function typeMultiPolygonZ(Fluent $column) - { - return $this->formatPostGisType('multipolygonz', $column); + return 'geometry'; } /** - * Format the column definition for a PostGIS spatial type. + * Create the column definition for a spatial Geography type. * - * @param string $type * @param \Illuminate\Support\Fluent $column * @return string */ - private function formatPostGisType($type, Fluent $column) + protected function typeGeography(Fluent $column) { - if ($column->isGeometry === null) { - return sprintf('geography(%s, %s)', $type, $column->projection ?? '4326'); - } - - if ($column->projection !== null) { - return sprintf('geometry(%s, %s)', $type, $column->projection); + if ($column->subtype) { + return sprintf('geography(%s%s)', + strtolower($column->subtype), + $column->srid ? ','.$column->srid : '' + ); } - return "geometry({$type})"; + return 'geography'; } /** @@ -1258,7 +1093,11 @@ protected function modifyNullable(Blueprint $blueprint, Fluent $column) protected function modifyDefault(Blueprint $blueprint, Fluent $column) { if ($column->change) { - return is_null($column->default) ? 'drop default' : 'set default '.$this->getDefaultValue($column->default); + if (! $column->autoIncrement || ! is_null($column->generatedAs)) { + return is_null($column->default) ? 'drop default' : 'set default '.$this->getDefaultValue($column->default); + } + + return null; } if (! is_null($column->default)) { @@ -1276,6 +1115,7 @@ protected function modifyDefault(Blueprint $blueprint, Fluent $column) protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { if (! $column->change + && ! $this->hasCommand($blueprint, 'primary') && (in_array($column->type, $this->serials) || ($column->generatedAs !== null)) && $column->autoIncrement) { return ' primary key'; @@ -1350,7 +1190,7 @@ protected function modifyGeneratedAs(Blueprint $blueprint, Fluent $column) } if ($column->change) { - $changes = ['drop identity if exists']; + $changes = $column->autoIncrement && is_null($sql) ? [] : ['drop identity if exists']; if (! is_null($sql)) { $changes[] = 'add '.$sql; diff --git a/src/Illuminate/Database/Schema/Grammars/RenameColumn.php b/src/Illuminate/Database/Schema/Grammars/RenameColumn.php deleted file mode 100644 index ff611c93160a..000000000000 --- a/src/Illuminate/Database/Schema/Grammars/RenameColumn.php +++ /dev/null @@ -1,93 +0,0 @@ -getDoctrineSchemaManager(); - $databasePlatform = $connection->getDoctrineConnection()->getDatabasePlatform(); - $databasePlatform->registerDoctrineTypeMapping('enum', 'string'); - - $column = $connection->getDoctrineColumn( - $grammar->getTablePrefix().$blueprint->getTable(), $command->from - ); - - return (array) $databasePlatform->getAlterTableSQL(static::getRenamedDiff( - $grammar, $blueprint, $command, $column, $schema - )); - } - - /** - * Get a new column instance with the new column name. - * - * @param \Illuminate\Database\Schema\Grammars\Grammar $grammar - * @param \Illuminate\Database\Schema\Blueprint $blueprint - * @param \Illuminate\Support\Fluent $command - * @param \Doctrine\DBAL\Schema\Column $column - * @param \Doctrine\DBAL\Schema\AbstractSchemaManager $schema - * @return \Doctrine\DBAL\Schema\TableDiff - */ - protected static function getRenamedDiff(Grammar $grammar, Blueprint $blueprint, Fluent $command, Column $column, SchemaManager $schema) - { - return static::setRenamedColumns( - $grammar->getDoctrineTableDiff($blueprint, $schema), $command, $column - ); - } - - /** - * Set the renamed columns on the table diff. - * - * @param \Doctrine\DBAL\Schema\TableDiff $tableDiff - * @param \Illuminate\Support\Fluent $command - * @param \Doctrine\DBAL\Schema\Column $column - * @return \Doctrine\DBAL\Schema\TableDiff - */ - protected static function setRenamedColumns(TableDiff $tableDiff, Fluent $command, Column $column) - { - return new TableDiff( - $tableDiff->getOldTable(), - $tableDiff->getAddedColumns(), - $tableDiff->getModifiedColumns(), - $tableDiff->getDroppedColumns(), - [$command->from => new Column($command->to, $column->getType(), self::getWritableColumnOptions($column))], - $tableDiff->getAddedIndexes(), - $tableDiff->getModifiedIndexes(), - $tableDiff->getDroppedIndexes(), - $tableDiff->getRenamedIndexes(), - $tableDiff->getAddedForeignKeys(), - $tableDiff->getModifiedColumns(), - $tableDiff->getDroppedForeignKeys(), - ); - } - - /** - * Get the writable column options. - * - * @param \Doctrine\DBAL\Schema\Column $column - * @return array - */ - private static function getWritableColumnOptions(Column $column) - { - return array_filter($column->toArray(), function (string $name) use ($column) { - return method_exists($column, 'set'.$name); - }, ARRAY_FILTER_USE_KEY); - } -} diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php old mode 100755 new mode 100644 index 744a9d691534..5d969c3eaf93 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -2,11 +2,12 @@ namespace Illuminate\Database\Schema\Grammars; -use Doctrine\DBAL\Schema\Index; -use Doctrine\DBAL\Schema\TableDiff; use Illuminate\Database\Connection; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Database\Schema\ColumnDefinition; +use Illuminate\Database\Schema\ForeignKeyDefinition; +use Illuminate\Database\Schema\IndexDefinition; use Illuminate\Support\Arr; use Illuminate\Support\Fluent; use RuntimeException; @@ -18,7 +19,7 @@ class SQLiteGrammar extends Grammar * * @var string[] */ - protected $modifiers = ['Increment', 'Nullable', 'Default', 'VirtualAs', 'StoredAs']; + protected $modifiers = ['Increment', 'Nullable', 'Default', 'Collate', 'VirtualAs', 'StoredAs']; /** * The columns available as serials. @@ -28,15 +29,18 @@ class SQLiteGrammar extends Grammar protected $serials = ['bigInteger', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger']; /** - * Compile the query to determine if a table exists. - * - * @deprecated Will be removed in a future Laravel version. + * Compile the query to determine the SQL text that describes the given object. * + * @param string $name + * @param string $type * @return string */ - public function compileTableExists() + public function compileSqlCreateStatement($name, $type = 'table') { - return "select * from sqlite_master where type = 'table' and name = ?"; + return sprintf('select "sql" from sqlite_master where type = %s and name = %s', + $this->wrap($type), + $this->wrap(str_replace('.', '__', $name)) + ); } /** @@ -76,43 +80,6 @@ public function compileViews() return "select name, sql as definition from sqlite_master where type = 'view' order by name"; } - /** - * Compile the SQL needed to retrieve all table names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileGetAllTables() - { - return 'select type, name from sqlite_master where type = \'table\' and name not like \'sqlite_%\''; - } - - /** - * Compile the SQL needed to retrieve all view names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileGetAllViews() - { - return 'select type, name from sqlite_master where type = \'view\''; - } - - /** - * Compile the query to determine the list of columns. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param string $table - * @return string - */ - public function compileColumnListing($table) - { - return 'pragma table_info('.$this->wrap(str_replace('.', '__', $table)).')'; - } - /** * Compile the query to determine the columns. * @@ -122,8 +89,8 @@ public function compileColumnListing($table) public function compileColumns($table) { return sprintf( - "select name, type, not 'notnull' as 'nullable', dflt_value as 'default', pk as 'primary' " - .'from pragma_table_info(%s) order by cid asc', + 'select name, type, not "notnull" as "nullable", dflt_value as "default", pk as "primary" ' + .'from pragma_table_xinfo(%s) order by cid asc', $this->wrap(str_replace('.', '__', $table)) ); } @@ -177,39 +144,24 @@ public function compileCreate(Blueprint $blueprint, Fluent $command) $blueprint->temporary ? 'create temporary' : 'create', $this->wrapTable($blueprint), implode(', ', $this->getColumns($blueprint)), - (string) $this->addForeignKeys($blueprint), - (string) $this->addPrimaryKeys($blueprint) + $this->addForeignKeys($this->getCommandsByName($blueprint, 'foreign')), + $this->addPrimaryKeys($this->getCommandByName($blueprint, 'primary')) ); } /** * Get the foreign key syntax for a table creation statement. * - * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Database\Schema\ForeignKeyDefinition[] $foreignKeys * @return string|null */ - protected function addForeignKeys(Blueprint $blueprint) + protected function addForeignKeys($foreignKeys) { - $foreigns = $this->getCommandsByName($blueprint, 'foreign'); - - return collect($foreigns)->reduce(function ($sql, $foreign) { + return collect($foreignKeys)->reduce(function ($sql, $foreign) { // Once we have all the foreign key commands for the table creation statement // we'll loop through each of them and add them to the create table SQL we // are building, since SQLite needs foreign keys on the tables creation. - $sql .= $this->getForeignKey($foreign); - - if (! is_null($foreign->onDelete)) { - $sql .= " on delete {$foreign->onDelete}"; - } - - // If this foreign key specifies the action to be taken on update we will add - // that to the statement here. We'll append it to this SQL and then return - // the SQL so we can keep adding any other foreign constraints onto this. - if (! is_null($foreign->onUpdate)) { - $sql .= " on update {$foreign->onUpdate}"; - } - - return $sql; + return $sql.$this->getForeignKey($foreign); }, ''); } @@ -224,22 +176,35 @@ protected function getForeignKey($foreign) // We need to columnize the columns that the foreign key is being defined for // so that it is a properly formatted list. Once we have done this, we can // return the foreign key SQL declaration to the calling method for use. - return sprintf(', foreign key(%s) references %s(%s)', + $sql = sprintf(', foreign key(%s) references %s(%s)', $this->columnize($foreign->columns), $this->wrapTable($foreign->on), $this->columnize((array) $foreign->references) ); + + if (! is_null($foreign->onDelete)) { + $sql .= " on delete {$foreign->onDelete}"; + } + + // If this foreign key specifies the action to be taken on update we will add + // that to the statement here. We'll append it to this SQL and then return + // this SQL so we can keep adding any other foreign constraints to this. + if (! is_null($foreign->onUpdate)) { + $sql .= " on update {$foreign->onUpdate}"; + } + + return $sql; } /** * Get the primary key syntax for a table creation statement. * - * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent|null $primary * @return string|null */ - protected function addPrimaryKeys(Blueprint $blueprint) + protected function addPrimaryKeys($primary) { - if (! is_null($primary = $this->getCommandByName($blueprint, 'primary'))) { + if (! is_null($primary)) { return ", primary key ({$this->columnize($primary->columns)})"; } } @@ -255,30 +220,99 @@ public function compileAdd(Blueprint $blueprint, Fluent $command) { $columns = $this->prefixArray('add column', $this->getColumns($blueprint)); - return collect($columns)->reject(function ($column) { - return preg_match('/as \(.*\) stored/', $column) > 0; - })->map(function ($column) use ($blueprint) { + return collect($columns)->map(function ($column) use ($blueprint) { return 'alter table '.$this->wrapTable($blueprint).' '.$column; })->all(); } /** - * Compile a rename column command. + * Compile a change column command into a series of SQL statements. * * @param \Illuminate\Database\Schema\Blueprint $blueprint * @param \Illuminate\Support\Fluent $command * @param \Illuminate\Database\Connection $connection * @return array|string + * + * @throws \RuntimeException */ - public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection) - { - return $connection->usingNativeSchemaOperations() - ? sprintf('alter table %s rename column %s to %s', - $this->wrapTable($blueprint), - $this->wrap($command->from), - $this->wrap($command->to) - ) - : parent::compileRenameColumn($blueprint, $command, $connection); + public function compileChange(Blueprint $blueprint, Fluent $command, Connection $connection) + { + $schema = $connection->getSchemaBuilder(); + $table = $blueprint->getTable(); + + $changedColumns = collect($blueprint->getChangedColumns()); + $columnNames = []; + $autoIncrementColumn = null; + + $columns = collect($schema->getColumns($table)) + ->map(function ($column) use ($blueprint, $changedColumns, &$columnNames, &$autoIncrementColumn) { + $column = $changedColumns->first(fn ($col) => $col->name === $column['name'], $column); + + if ($column instanceof Fluent) { + $name = $this->wrap($column); + $columnNames[] = $name; + $autoIncrementColumn = $column->autoIncrement ? $column->name : $autoIncrementColumn; + + return $this->addModifiers($name.' '.$this->getType($column), $blueprint, $column); + } else { + $name = $this->wrap($column['name']); + $columnNames[] = $name; + $autoIncrementColumn = $column['auto_increment'] ? $column['name'] : $autoIncrementColumn; + + return $this->addModifiers($name.' '.$column['type'], $blueprint, + new ColumnDefinition([ + 'change' => true, + 'type' => $column['type_name'], + 'nullable' => $column['nullable'], + 'default' => $column['default'] ? new Expression($column['default']) : null, + 'autoIncrement' => $column['auto_increment'], + 'collation' => $column['collation'], + 'comment' => $column['comment'], + ]) + ); + } + })->all(); + + $foreignKeys = collect($schema->getForeignKeys($table))->map(fn ($foreignKey) => new ForeignKeyDefinition([ + 'columns' => $foreignKey['columns'], + 'on' => $foreignKey['foreign_table'], + 'references' => $foreignKey['foreign_columns'], + 'onUpdate' => $foreignKey['on_update'], + 'onDelete' => $foreignKey['on_delete'], + ]))->all(); + + [$primary, $indexes] = collect($schema->getIndexes($table))->map(fn ($index) => new IndexDefinition([ + 'name' => match (true) { + $index['primary'] => 'primary', + $index['unique'] => 'unique', + default => 'index', + }, + 'index' => $index['name'], + 'columns' => $index['columns'], + ]))->partition(fn ($index) => $index->name === 'primary'); + + $indexes = collect($indexes)->reject(fn ($index) => str_starts_with('sqlite_', $index->index))->map( + fn ($index) => $this->{'compile'.ucfirst($index->name)}($blueprint, $index) + )->all(); + + $tempTable = $this->wrap('__temp__'.$blueprint->getPrefix().$table); + $table = $this->wrapTable($blueprint); + $columnNames = implode(', ', $columnNames); + + $foreignKeyConstraintsEnabled = $connection->scalar('pragma foreign_keys'); + + return array_filter(array_merge([ + $foreignKeyConstraintsEnabled ? $this->compileDisableForeignKeyConstraints() : null, + sprintf('create table %s (%s%s%s)', + $tempTable, + implode(', ', $columns), + $this->addForeignKeys($foreignKeys), + $autoIncrementColumn ? '' : $this->addPrimaryKeys($primary->first()) + ), + sprintf('insert into %s (%s) select %s from %s', $tempTable, $columnNames, $columnNames, $table), + sprintf('drop table %s', $table), + sprintf('alter table %s rename to %s', $tempTable, $table), + ], $indexes, [$foreignKeyConstraintsEnabled ? $this->compileEnableForeignKeyConstraints() : null])); } /** @@ -403,45 +437,11 @@ public function compileRebuild() */ public function compileDropColumn(Blueprint $blueprint, Fluent $command, Connection $connection) { - if ($connection->usingNativeSchemaOperations()) { - $table = $this->wrapTable($blueprint); - - $columns = $this->prefixArray('drop column', $this->wrapArray($command->columns)); + $table = $this->wrapTable($blueprint); - return collect($columns)->map(fn ($column) => 'alter table '.$table.' '.$column - )->all(); - } else { - $tableDiff = $this->getDoctrineTableDiff( - $blueprint, $schema = $connection->getDoctrineSchemaManager() - ); + $columns = $this->prefixArray('drop column', $this->wrapArray($command->columns)); - $droppedColumns = []; - - foreach ($command->columns as $name) { - $droppedColumns[$name] = $connection->getDoctrineColumn( - $this->getTablePrefix().$blueprint->getTable(), $name - ); - } - - $platform = $connection->getDoctrineConnection()->getDatabasePlatform(); - - return (array) $platform->getAlterTableSQL( - new TableDiff( - $tableDiff->getOldTable(), - $tableDiff->getAddedColumns(), - $tableDiff->getModifiedColumns(), - $droppedColumns, - $tableDiff->getRenamedColumns(), - $tableDiff->getAddedIndexes(), - $tableDiff->getModifiedIndexes(), - $tableDiff->getDroppedIndexes(), - $tableDiff->getRenamedIndexes(), - $tableDiff->getAddedForeignKeys(), - $tableDiff->getModifiedColumns(), - $tableDiff->getDroppedForeignKeys(), - ) - ); - } + return collect($columns)->map(fn ($column) => 'alter table '.$table.' '.$column)->all(); } /** @@ -512,26 +512,32 @@ public function compileRename(Blueprint $blueprint, Fluent $command) */ public function compileRenameIndex(Blueprint $blueprint, Fluent $command, Connection $connection) { - $schemaManager = $connection->getDoctrineSchemaManager(); - - $indexes = $schemaManager->listTableIndexes($this->getTablePrefix().$blueprint->getTable()); + $indexes = $connection->getSchemaBuilder()->getIndexes($blueprint->getTable()); - $index = Arr::get($indexes, $command->from); + $index = Arr::first($indexes, fn ($index) => $index['name'] === $command->from); if (! $index) { throw new RuntimeException("Index [{$command->from}] does not exist."); } - $newIndex = new Index( - $command->to, $index->getColumns(), $index->isUnique(), - $index->isPrimary(), $index->getFlags(), $index->getOptions() - ); + if ($index['primary']) { + throw new RuntimeException('SQLite does not support altering primary keys.'); + } - $platform = $connection->getDoctrineConnection()->getDatabasePlatform(); + if ($index['unique']) { + return [ + $this->compileDropUnique($blueprint, new IndexDefinition(['index' => $index['name']])), + $this->compileUnique($blueprint, + new IndexDefinition(['index' => $command->to, 'columns' => $index['columns']]) + ), + ]; + } return [ - $platform->getDropIndexSQL($command->from, $this->getTablePrefix().$blueprint->getTable()), - $platform->getCreateIndexSQL($newIndex, $this->getTablePrefix().$blueprint->getTable()), + $this->compileDropIndex($blueprint, new IndexDefinition(['index' => $index['name']])), + $this->compileIndex($blueprint, + new IndexDefinition(['index' => $command->to, 'columns' => $index['columns']]) + ), ]; } @@ -923,86 +929,20 @@ protected function typeMacAddress(Fluent $column) * @param \Illuminate\Support\Fluent $column * @return string */ - public function typeGeometry(Fluent $column) + protected function typeGeometry(Fluent $column) { return 'geometry'; } /** - * Create the column definition for a spatial Point type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typePoint(Fluent $column) - { - return 'point'; - } - - /** - * Create the column definition for a spatial LineString type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeLineString(Fluent $column) - { - return 'linestring'; - } - - /** - * Create the column definition for a spatial Polygon type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typePolygon(Fluent $column) - { - return 'polygon'; - } - - /** - * Create the column definition for a spatial GeometryCollection type. + * Create the column definition for a spatial Geography type. * * @param \Illuminate\Support\Fluent $column * @return string */ - public function typeGeometryCollection(Fluent $column) + protected function typeGeography(Fluent $column) { - return 'geometrycollection'; - } - - /** - * Create the column definition for a spatial MultiPoint type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeMultiPoint(Fluent $column) - { - return 'multipoint'; - } - - /** - * Create the column definition for a spatial MultiLineString type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeMultiLineString(Fluent $column) - { - return 'multilinestring'; - } - - /** - * Create the column definition for a spatial MultiPolygon type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeMultiPolygon(Fluent $column) - { - return 'multipolygon'; + return $this->typeGeometry($column); } /** @@ -1111,6 +1051,20 @@ protected function modifyIncrement(Blueprint $blueprint, Fluent $column) } } + /** + * Get the SQL for a collation column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyCollate(Blueprint $blueprint, Fluent $column) + { + if (! is_null($column->collation)) { + return " collate '{$column->collation}'"; + } + } + /** * Wrap the given JSON selector. * diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index dd56619c74d9..b719f127f3dc 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -66,18 +66,6 @@ public function compileDropDatabaseIfExists($name) ); } - /** - * Compile the query to determine if a table exists. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileTableExists() - { - return "select * from sys.sysobjects where id = object_id(?) and xtype in ('U', 'V')"; - } - /** * Compile the query to determine the tables. * @@ -85,7 +73,7 @@ public function compileTableExists() */ public function compileTables() { - return 'select t.name as name, SCHEMA_NAME(t.schema_id) as [schema], sum(u.total_pages) * 8 * 1024 as size ' + return 'select t.name as name, schema_name(t.schema_id) as [schema], sum(u.total_pages) * 8 * 1024 as size ' .'from sys.tables as t ' .'join sys.partitions as p on p.object_id = t.object_id ' .'join sys.allocation_units as u on u.container_id = p.hobt_id ' @@ -100,55 +88,19 @@ public function compileTables() */ public function compileViews() { - return 'select name, SCHEMA_NAME(v.schema_id) as [schema], definition from sys.views as v ' + return 'select name, schema_name(v.schema_id) as [schema], definition from sys.views as v ' .'inner join sys.sql_modules as m on v.object_id = m.object_id ' .'order by name'; } - /** - * Compile the SQL needed to retrieve all table names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileGetAllTables() - { - return "select name, type from sys.tables where type = 'U'"; - } - - /** - * Compile the SQL needed to retrieve all view names. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return string - */ - public function compileGetAllViews() - { - return "select name, type from sys.objects where type = 'V'"; - } - - /** - * Compile the query to determine the list of columns. - * - * @deprecated Will be removed in a future Laravel version. - * - * @param string $table - * @return string - */ - public function compileColumnListing($table) - { - return "select name from sys.columns where object_id = object_id('$table')"; - } - /** * Compile the query to determine the columns. * + * @param string $schema * @param string $table * @return string */ - public function compileColumns($table) + public function compileColumns($schema, $table) { return sprintf( 'select col.name, type.name as type_name, ' @@ -162,18 +114,21 @@ public function compileColumns($table) .'join sys.schemas as scm on obj.schema_id = scm.schema_id ' .'left join sys.default_constraints def on col.default_object_id = def.object_id and col.object_id = def.parent_object_id ' ."left join sys.extended_properties as prop on obj.object_id = prop.major_id and col.column_id = prop.minor_id and prop.name = 'MS_Description' " - ."where obj.type = 'U' and obj.name = %s and scm.name = SCHEMA_NAME()", + ."where obj.type in ('U', 'V') and obj.name = %s and scm.name = %s " + .'order by col.column_id', $this->quoteString($table), + $schema ? $this->quoteString($schema) : 'schema_name()', ); } /** * Compile the query to determine the indexes. * + * @param string $schema * @param string $table * @return string */ - public function compileIndexes($table) + public function compileIndexes($schema, $table) { return sprintf( "select idx.name as name, string_agg(col.name, ',') within group (order by idxcol.key_ordinal) as columns, " @@ -183,19 +138,21 @@ public function compileIndexes($table) .'join sys.schemas as scm on tbl.schema_id = scm.schema_id ' .'join sys.index_columns as idxcol on idx.object_id = idxcol.object_id and idx.index_id = idxcol.index_id ' .'join sys.columns as col on idxcol.object_id = col.object_id and idxcol.column_id = col.column_id ' - .'where tbl.name = %s and scm.name = SCHEMA_NAME() ' + .'where tbl.name = %s and scm.name = %s ' .'group by idx.name, idx.type_desc, idx.is_unique, idx.is_primary_key', $this->quoteString($table), + $schema ? $this->quoteString($schema) : 'schema_name()', ); } /** * Compile the query to determine the foreign keys. * + * @param string $schema * @param string $table * @return string */ - public function compileForeignKeys($table) + public function compileForeignKeys($schema, $table) { return sprintf( 'select fk.name as name, ' @@ -212,9 +169,10 @@ public function compileForeignKeys($table) .'join sys.tables as ft on ft.object_id = fk.referenced_object_id ' .'join sys.schemas as fs on ft.schema_id = fs.schema_id ' .'join sys.columns as fc on fkc.referenced_object_id = fc.object_id and fkc.referenced_column_id = fc.column_id ' - .'where lt.name = %s and ls.name = SCHEMA_NAME() ' + .'where lt.name = %s and ls.name = %s ' .'group by fk.name, fs.name, ft.name, fk.update_referential_action_desc, fk.delete_referential_action_desc', - $this->quoteString($table) + $this->quoteString($table), + $schema ? $this->quoteString($schema) : 'schema_name()', ); } @@ -257,12 +215,10 @@ public function compileAdd(Blueprint $blueprint, Fluent $command) */ public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Connection $connection) { - return $connection->usingNativeSchemaOperations() - ? sprintf("sp_rename '%s', %s, 'COLUMN'", - $this->wrap($blueprint->getTable().'.'.$command->from), - $this->wrap($command->to) - ) - : parent::compileRenameColumn($blueprint, $command, $connection); + return sprintf("sp_rename %s, %s, N'COLUMN'", + $this->quoteString($this->wrapTable($blueprint).'.'.$this->wrap($command->from)), + $this->wrap($command->to) + ); } /** @@ -277,10 +233,6 @@ public function compileRenameColumn(Blueprint $blueprint, Fluent $command, Conne */ public function compileChange(Blueprint $blueprint, Fluent $command, Connection $connection) { - if (! $connection->usingNativeSchemaOperations()) { - return parent::compileChange($blueprint, $command, $connection); - } - $changes = [$this->compileDropDefaultConstraint($blueprint, $command)]; foreach ($blueprint->getChangedColumns() as $column) { @@ -405,8 +357,8 @@ public function compileDrop(Blueprint $blueprint, Fluent $command) */ public function compileDropIfExists(Blueprint $blueprint, Fluent $command) { - return sprintf('if exists (select * from sys.sysobjects where id = object_id(%s, \'U\')) drop table %s', - "'".str_replace("'", "''", $this->getTablePrefix().$blueprint->getTable())."'", + return sprintf('if object_id(%s, \'U\') is not null drop table %s', + $this->quoteString($this->wrapTable($blueprint)), $this->wrapTable($blueprint) ); } @@ -450,12 +402,13 @@ public function compileDropDefaultConstraint(Blueprint $blueprint, Fluent $comma ? "'".collect($blueprint->getChangedColumns())->pluck('name')->implode("','")."'" : "'".implode("','", $command->columns)."'"; - $tableName = $this->getTablePrefix().$blueprint->getTable(); + $table = $this->wrapTable($blueprint); + $tableName = $this->quoteString($this->wrapTable($blueprint)); $sql = "DECLARE @sql NVARCHAR(MAX) = '';"; - $sql .= "SELECT @sql += 'ALTER TABLE [dbo].[{$tableName}] DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' "; + $sql .= "SELECT @sql += 'ALTER TABLE $table DROP CONSTRAINT ' + OBJECT_NAME([default_object_id]) + ';' "; $sql .= 'FROM sys.columns '; - $sql .= "WHERE [object_id] = OBJECT_ID('[dbo].[{$tableName}]') AND [name] in ({$columns}) AND [default_object_id] <> 0;"; + $sql .= "WHERE [object_id] = OBJECT_ID($tableName) AND [name] in ($columns) AND [default_object_id] <> 0;"; $sql .= 'EXEC(@sql)'; return $sql; @@ -538,9 +491,10 @@ public function compileDropForeign(Blueprint $blueprint, Fluent $command) */ public function compileRename(Blueprint $blueprint, Fluent $command) { - $from = $this->wrapTable($blueprint); - - return "sp_rename {$from}, ".$this->wrapTable($command->to); + return sprintf('sp_rename %s, %s', + $this->quoteString($this->wrapTable($blueprint)), + $this->wrapTable($command->to) + ); } /** @@ -552,8 +506,8 @@ public function compileRename(Blueprint $blueprint, Fluent $command) */ public function compileRenameIndex(Blueprint $blueprint, Fluent $command) { - return sprintf("sp_rename N'%s', %s, N'INDEX'", - $this->wrap($blueprint->getTable().'.'.$command->from), + return sprintf("sp_rename %s, %s, N'INDEX'", + $this->quoteString($this->wrapTable($blueprint).'.'.$this->wrap($command->from)), $this->wrap($command->to) ); } @@ -962,84 +916,18 @@ protected function typeMacAddress(Fluent $column) * @param \Illuminate\Support\Fluent $column * @return string */ - public function typeGeometry(Fluent $column) + protected function typeGeometry(Fluent $column) { - return 'geography'; - } - - /** - * Create the column definition for a spatial Point type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typePoint(Fluent $column) - { - return 'geography'; - } - - /** - * Create the column definition for a spatial LineString type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeLineString(Fluent $column) - { - return 'geography'; - } - - /** - * Create the column definition for a spatial Polygon type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typePolygon(Fluent $column) - { - return 'geography'; - } - - /** - * Create the column definition for a spatial GeometryCollection type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeGeometryCollection(Fluent $column) - { - return 'geography'; - } - - /** - * Create the column definition for a spatial MultiPoint type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeMultiPoint(Fluent $column) - { - return 'geography'; - } - - /** - * Create the column definition for a spatial MultiLineString type. - * - * @param \Illuminate\Support\Fluent $column - * @return string - */ - public function typeMultiLineString(Fluent $column) - { - return 'geography'; + return 'geometry'; } /** - * Create the column definition for a spatial MultiPolygon type. + * Create the column definition for a spatial Geography type. * * @param \Illuminate\Support\Fluent $column * @return string */ - public function typeMultiPolygon(Fluent $column) + protected function typeGeography(Fluent $column) { return 'geography'; } @@ -1107,7 +995,7 @@ protected function modifyDefault(Blueprint $blueprint, Fluent $column) protected function modifyIncrement(Blueprint $blueprint, Fluent $column) { if (! $column->change && in_array($column->type, $this->serials) && $column->autoIncrement) { - return ' identity primary key'; + return $this->hasCommand($blueprint, 'primary') ? ' identity' : ' identity primary key'; } } diff --git a/src/Illuminate/Database/Schema/MariaDbBuilder.php b/src/Illuminate/Database/Schema/MariaDbBuilder.php new file mode 100755 index 000000000000..012befe3802e --- /dev/null +++ b/src/Illuminate/Database/Schema/MariaDbBuilder.php @@ -0,0 +1,8 @@ +connectionString().' --no-tablespaces --skip-add-locks --skip-comments --skip-set-charset --tz-utc --column-statistics=0'; + + return $command.' "${:LARAVEL_LOAD_DATABASE}"'; + } +} diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index 0c537ba980cd..943ae9f4fadf 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -58,34 +58,6 @@ public function getViews() ); } - /** - * Get all of the table names for the database. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return array - */ - public function getAllTables() - { - return $this->connection->select( - $this->grammar->compileGetAllTables() - ); - } - - /** - * Get all of the view names for the database. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return array - */ - public function getAllViews() - { - return $this->connection->select( - $this->grammar->compileGetAllViews() - ); - } - /** * Get the columns for a given table. * diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index 9cf829721ad2..be6512bbf2f7 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -60,50 +60,36 @@ public function hasTable($table) } /** - * Get the user-defined types that belong to the database. + * Determine if the given view exists. * - * @return array + * @param string $view + * @return bool */ - public function getTypes() + public function hasView($view) { - return $this->connection->getPostProcessor()->processTypes( - $this->connection->selectFromWriteConnection($this->grammar->compileTypes()) - ); - } + [$schema, $view] = $this->parseSchemaAndTable($view); - /** - * Get all of the table names for the database. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return array - */ - public function getAllTables() - { - return $this->connection->select( - $this->grammar->compileGetAllTables( - $this->parseSearchPath( - $this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') - ) - ) - ); + $view = $this->connection->getTablePrefix().$view; + + foreach ($this->getViews() as $value) { + if (strtolower($view) === strtolower($value['name']) + && strtolower($schema) === strtolower($value['schema'])) { + return true; + } + } + + return false; } /** - * Get all of the view names for the database. - * - * @deprecated Will be removed in a future Laravel version. + * Get the user-defined types that belong to the database. * * @return array */ - public function getAllViews() + public function getTypes() { - return $this->connection->select( - $this->grammar->compileGetAllViews( - $this->parseSearchPath( - $this->connection->getConfig('search_path') ?: $this->connection->getConfig('schema') - ) - ) + return $this->connection->getPostProcessor()->processTypes( + $this->connection->selectFromWriteConnection($this->grammar->compileTypes()) ); } @@ -166,20 +152,6 @@ public function dropAllViews() ); } - /** - * Get all of the type names for the database. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return array - */ - public function getAllTypes() - { - return $this->connection->select( - $this->grammar->compileGetAllTypes() - ); - } - /** * Drop all types from the database. * @@ -289,7 +261,7 @@ protected function parseSchemaAndTable($reference) if (count($parts) > 2) { $database = $parts[0]; - throw new InvalidArgumentException("Using 3-parts reference is not supported, you may use `Schema::connection('$database')` instead."); + throw new InvalidArgumentException("Using three-part reference is not supported, you may use `Schema::connection('$database')` instead."); } // We will use the default schema unless the schema has been specified in the diff --git a/src/Illuminate/Database/Schema/SQLiteBuilder.php b/src/Illuminate/Database/Schema/SQLiteBuilder.php index 8ae272d767b6..ef19684172fa 100644 --- a/src/Illuminate/Database/Schema/SQLiteBuilder.php +++ b/src/Illuminate/Database/Schema/SQLiteBuilder.php @@ -52,30 +52,18 @@ public function getTables() } /** - * Get all of the table names for the database. - * - * @deprecated Will be removed in a future Laravel version. + * Get the columns for a given table. * + * @param string $table * @return array */ - public function getAllTables() + public function getColumns($table) { - return $this->connection->select( - $this->grammar->compileGetAllTables() - ); - } + $table = $this->connection->getTablePrefix().$table; - /** - * Get all of the view names for the database. - * - * @deprecated Will be removed in a future Laravel version. - * - * @return array - */ - public function getAllViews() - { - return $this->connection->select( - $this->grammar->compileGetAllViews() + return $this->connection->getPostProcessor()->processColumns( + $this->connection->selectFromWriteConnection($this->grammar->compileColumns($table)), + $this->connection->scalar($this->grammar->compileSqlCreateStatement($table)) ); } diff --git a/src/Illuminate/Database/Schema/SqlServerBuilder.php b/src/Illuminate/Database/Schema/SqlServerBuilder.php index e7717534f803..5f3edb6a4e41 100644 --- a/src/Illuminate/Database/Schema/SqlServerBuilder.php +++ b/src/Illuminate/Database/Schema/SqlServerBuilder.php @@ -30,6 +30,50 @@ public function dropDatabaseIfExists($name) ); } + /** + * Determine if the given table exists. + * + * @param string $table + * @return bool + */ + public function hasTable($table) + { + [$schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + foreach ($this->getTables() as $value) { + if (strtolower($table) === strtolower($value['name']) + && strtolower($schema) === strtolower($value['schema'])) { + return true; + } + } + + return false; + } + + /** + * Determine if the given view exists. + * + * @param string $view + * @return bool + */ + public function hasView($view) + { + [$schema, $view] = $this->parseSchemaAndTable($view); + + $view = $this->connection->getTablePrefix().$view; + + foreach ($this->getViews() as $value) { + if (strtolower($view) === strtolower($value['name']) + && strtolower($schema) === strtolower($value['schema'])) { + return true; + } + } + + return false; + } + /** * Drop all tables from the database. * @@ -53,30 +97,74 @@ public function dropAllViews() } /** - * Drop all tables from the database. - * - * @deprecated Will be removed in a future Laravel version. + * Get the columns for a given table. * + * @param string $table * @return array */ - public function getAllTables() + public function getColumns($table) { - return $this->connection->select( - $this->grammar->compileGetAllTables() + [$schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + $results = $this->connection->selectFromWriteConnection( + $this->grammar->compileColumns($schema, $table) ); + + return $this->connection->getPostProcessor()->processColumns($results); } /** - * Get all of the view names for the database. + * Get the indexes for a given table. * - * @deprecated Will be removed in a future Laravel version. + * @param string $table + * @return array + */ + public function getIndexes($table) + { + [$schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processIndexes( + $this->connection->selectFromWriteConnection($this->grammar->compileIndexes($schema, $table)) + ); + } + + /** + * Get the foreign keys for a given table. * + * @param string $table * @return array */ - public function getAllViews() + public function getForeignKeys($table) { - return $this->connection->select( - $this->grammar->compileGetAllViews() + [$schema, $table] = $this->parseSchemaAndTable($table); + + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processForeignKeys( + $this->connection->selectFromWriteConnection($this->grammar->compileForeignKeys($schema, $table)) ); } + + /** + * Parse the database object reference and extract the schema and table. + * + * @param string $reference + * @return array + */ + protected function parseSchemaAndTable($reference) + { + $parts = array_pad(explode('.', $reference, 2), -2, 'dbo'); + + if (str_contains($parts[1], '.')) { + $database = $parts[0]; + + throw new InvalidArgumentException("Using three-part reference is not supported, you may use `Schema::connection('$database')` instead."); + } + + return $parts; + } } diff --git a/src/Illuminate/Database/SqlServerConnection.php b/src/Illuminate/Database/SqlServerConnection.php index e376e6fa6c38..f977df57cc68 100755 --- a/src/Illuminate/Database/SqlServerConnection.php +++ b/src/Illuminate/Database/SqlServerConnection.php @@ -4,7 +4,6 @@ use Closure; use Exception; -use Illuminate\Database\PDO\SqlServerDriver; use Illuminate\Database\Query\Grammars\SqlServerGrammar as QueryGrammar; use Illuminate\Database\Query\Processors\SqlServerProcessor; use Illuminate\Database\Schema\Grammars\SqlServerGrammar as SchemaGrammar; @@ -139,14 +138,4 @@ protected function getDefaultPostProcessor() { return new SqlServerProcessor; } - - /** - * Get the Doctrine DBAL driver. - * - * @return \Illuminate\Database\PDO\SqlServerDriver - */ - protected function getDoctrineDriver() - { - return new SqlServerDriver; - } } diff --git a/src/Illuminate/Database/composer.json b/src/Illuminate/Database/composer.json index c840493bd621..bcbb837c0818 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -17,7 +17,7 @@ "require": { "php": "^8.2", "ext-pdo": "*", - "brick/math": "^0.9.3|^0.10.2|^0.11", + "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12", "illuminate/collections": "^11.0", "illuminate/container": "^11.0", "illuminate/contracts": "^11.0", @@ -36,7 +36,6 @@ }, "suggest": { "ext-filter": "Required to use the Postgres database driver.", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^4.0).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.21).", "illuminate/console": "Required to use the database commands (^11.0).", "illuminate/events": "Required to use the observers with Eloquent (^11.0).", diff --git a/src/Illuminate/Encryption/Encrypter.php b/src/Illuminate/Encryption/Encrypter.php index 8a8c6d85b0fc..003bbfbffc87 100755 --- a/src/Illuminate/Encryption/Encrypter.php +++ b/src/Illuminate/Encryption/Encrypter.php @@ -17,6 +17,13 @@ class Encrypter implements EncrypterContract, StringEncrypter */ protected $key; + /** + * The previous / legacy encryption keys. + * + * @var array + */ + protected $previousKeys = []; + /** * The algorithm used for encryption. * @@ -113,7 +120,7 @@ public function encrypt($value, $serialize = true) $mac = self::$supportedCiphers[strtolower($this->cipher)]['aead'] ? '' // For AEAD-algorithms, the tag / MAC is returned by openssl_encrypt... - : $this->hash($iv, $value); + : $this->hash($iv, $value, $this->key); $json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES); @@ -159,9 +166,15 @@ public function decrypt($payload, $unserialize = true) // Here we will decrypt the value. If we are able to successfully decrypt it // we will then unserialize it and return it out to the caller. If we are // unable to decrypt this value we will throw out an exception message. - $decrypted = \openssl_decrypt( - $payload['value'], strtolower($this->cipher), $this->key, 0, $iv, $tag ?? '' - ); + foreach ($this->getAllKeys() as $key) { + $decrypted = \openssl_decrypt( + $payload['value'], strtolower($this->cipher), $key, 0, $iv, $tag ?? '' + ); + + if ($decrypted !== false) { + break; + } + } if ($decrypted === false) { throw new DecryptException('Could not decrypt the data.'); @@ -188,11 +201,12 @@ public function decryptString($payload) * * @param string $iv * @param mixed $value + * @param string $key * @return string */ - protected function hash($iv, $value) + protected function hash($iv, $value, $key) { - return hash_hmac('sha256', $iv.$value, $this->key); + return hash_hmac('sha256', $iv.$value, $key); } /** @@ -258,9 +272,17 @@ protected function validPayload($payload) */ protected function validMac(array $payload) { - return hash_equals( - $this->hash($payload['iv'], $payload['value']), $payload['mac'] - ); + foreach ($this->getAllKeys() as $key) { + $valid = hash_equals( + $this->hash($payload['iv'], $payload['value'], $key), $payload['mac'] + ); + + if ($valid === true) { + return true; + } + } + + return false; } /** @@ -289,4 +311,45 @@ public function getKey() { return $this->key; } + + /** + * Get the current encryption key and all previous encryption keys. + * + * @return array + */ + public function getAllKeys() + { + return [$this->key, ...$this->previousKeys]; + } + + /** + * Get the previous encryption keys. + * + * @return array + */ + public function getPreviousKeys() + { + return $this->previousKeys; + } + + /** + * Set the previous / legacy encryption keys that should be utilized if decryption fails. + * + * @param array $key + * @return $this + */ + public function previousKeys(array $keys) + { + foreach ($keys as $key) { + if (! static::supported($key, $this->cipher)) { + $ciphers = implode(', ', array_keys(self::$supportedCiphers)); + + throw new RuntimeException("Unsupported cipher or incorrect key length. Supported ciphers are: {$ciphers}."); + } + } + + $this->previousKeys = $keys; + + return $this; + } } diff --git a/src/Illuminate/Encryption/EncryptionServiceProvider.php b/src/Illuminate/Encryption/EncryptionServiceProvider.php index a4d27a3720b9..5307042792af 100755 --- a/src/Illuminate/Encryption/EncryptionServiceProvider.php +++ b/src/Illuminate/Encryption/EncryptionServiceProvider.php @@ -29,7 +29,11 @@ protected function registerEncrypter() $this->app->singleton('encrypter', function ($app) { $config = $app->make('config')->get('app'); - return new Encrypter($this->parseKey($config), $config['cipher']); + return (new Encrypter($this->parseKey($config), $config['cipher'])) + ->previousKeys(array_map( + fn ($key) => $this->parseKey(['key' => $key]), + $config['previous_keys'] ?? [] + )); }); } diff --git a/src/Illuminate/Filesystem/Filesystem.php b/src/Illuminate/Filesystem/Filesystem.php index 23fc17eeb03c..c9485337c299 100644 --- a/src/Illuminate/Filesystem/Filesystem.php +++ b/src/Illuminate/Filesystem/Filesystem.php @@ -546,7 +546,7 @@ public function hasSameHash($firstFile, $secondFile) { $hash = @md5_file($firstFile); - return $hash && $hash === @md5_file($secondFile); + return $hash && hash_equals($hash, (string) @md5_file($secondFile)); } /** diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index a8e260d246d7..088135358fcf 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -237,7 +237,7 @@ public function directoryMissing($path) } /** - * Get the full path for the file at the given "short" path. + * Get the full path to the file that exists at the given relative path. * * @param string $path * @return string diff --git a/src/Illuminate/Filesystem/LockableFile.php b/src/Illuminate/Filesystem/LockableFile.php index 8b2de765eaad..d354b884036a 100644 --- a/src/Illuminate/Filesystem/LockableFile.php +++ b/src/Illuminate/Filesystem/LockableFile.php @@ -2,7 +2,6 @@ namespace Illuminate\Filesystem; -use Exception; use Illuminate\Contracts\Filesystem\LockTimeoutException; class LockableFile @@ -67,11 +66,7 @@ protected function ensureDirectoryExists($path) */ protected function createResource($path, $mode) { - $this->handle = @fopen($path, $mode); - - if (! $this->handle) { - throw new Exception('Unable to create lockable file: '.$path.'. Please ensure you have permission to create files in this location.'); - } + $this->handle = fopen($path, $mode); } /** diff --git a/src/Illuminate/Filesystem/composer.json b/src/Illuminate/Filesystem/composer.json index b46be7d136d2..95ee6851a485 100644 --- a/src/Illuminate/Filesystem/composer.json +++ b/src/Illuminate/Filesystem/composer.json @@ -24,7 +24,10 @@ "autoload": { "psr-4": { "Illuminate\\Filesystem\\": "" - } + }, + "files": [ + "functions.php" + ] }, "extra": { "branch-alias": { diff --git a/src/Illuminate/Filesystem/functions.php b/src/Illuminate/Filesystem/functions.php new file mode 100644 index 000000000000..af39716f6e4f --- /dev/null +++ b/src/Illuminate/Filesystem/functions.php @@ -0,0 +1,25 @@ + $path) { + if (empty($path)) { + unset($paths[$index]); + } else { + $paths[$index] = DIRECTORY_SEPARATOR.ltrim($path, DIRECTORY_SEPARATOR); + } + } + + return $basePath.implode('', $paths); + } +} diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 7679ed757c94..a6b471f39409 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -3,6 +3,7 @@ namespace Illuminate\Foundation; use Closure; +use Composer\Autoload\ClassLoader; use Illuminate\Container\Container; use Illuminate\Contracts\Console\Kernel as ConsoleKernelContract; use Illuminate\Contracts\Foundation\Application as ApplicationContract; @@ -32,6 +33,8 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\HttpKernelInterface; +use function Illuminate\Filesystem\join_paths; + class Application extends Container implements ApplicationContract, CachesConfiguration, CachesRoutes, HttpKernelInterface { use Macroable; @@ -217,19 +220,34 @@ public function __construct($basePath = null) /** * Begin configuring a new Laravel application instance. * - * @param string|null $baseDirectory - * @return \Illuminate\Foundation\ApplicationBuilder + * @param string|null $basePath + * @return \Illuminate\Foundation\Configuration\ApplicationBuilder */ - public static function configure(string $baseDirectory = null) + public static function configure(string $basePath = null) { - $baseDirectory = $ENV['APP_BASE_PATH'] ?? ($baseDirectory ?: dirname(dirname( - debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0]['file'] - ))); + $basePath = match (true) { + is_string($basePath) => $basePath, + default => static::inferBasePath(), + }; - return (new Configuration\ApplicationBuilder(new static($baseDirectory))) + return (new Configuration\ApplicationBuilder(new static($basePath))) ->withKernels() ->withEvents() - ->withCommands(); + ->withCommands() + ->withProviders(); + } + + /** + * Infer the application's base directory from the environment. + * + * @return string + */ + public static function inferBasePath() + { + return match (true) { + isset($_ENV['APP_BASE_PATH']) => $_ENV['APP_BASE_PATH'], + default => dirname(array_keys(ClassLoader::getRegisteredLoaders())[0]), + }; } /** @@ -624,7 +642,7 @@ public function viewPath($path = '') */ public function joinPaths($basePath, $path = '') { - return $basePath.($path != '' ? DIRECTORY_SEPARATOR.ltrim($path, DIRECTORY_SEPARATOR) : ''); + return join_paths($basePath, $path); } /** diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php index ee890cff2705..90bc446dca4e 100644 --- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php @@ -7,7 +7,9 @@ use Illuminate\Contracts\Debug\ExceptionHandler; use Illuminate\Contracts\Foundation\Application; use Illuminate\Log\LogManager; +use Illuminate\Support\Env; use Monolog\Handler\NullHandler; +use PHPUnit\Runner\ErrorHandler; use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\ErrorHandler\Error\FatalError; use Throwable; @@ -36,7 +38,7 @@ class HandleExceptions */ public function bootstrap(Application $app) { - self::$reservedMemory = str_repeat('x', 32768); + static::$reservedMemory = str_repeat('x', 32768); static::$app = $app; @@ -118,7 +120,7 @@ protected function shouldIgnoreDeprecationErrors() { return ! class_exists(LogManager::class) || ! static::$app->hasBeenBootstrapped() - || static::$app->runningUnitTests(); + || (static::$app->runningUnitTests() && ! Env::get('LOG_DEPRECATIONS_WHILE_TESTING')); } /** @@ -176,7 +178,7 @@ protected function ensureNullLogDriverIsConfigured() */ public function handleException(Throwable $e) { - self::$reservedMemory = null; + static::$reservedMemory = null; try { $this->getExceptionHandler()->report($e); @@ -224,7 +226,7 @@ protected function renderHttpResponse(Throwable $e) */ public function handleShutdown() { - self::$reservedMemory = null; + static::$reservedMemory = null; if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) { $this->handleException($this->fatalErrorFromPhpError($error, 0)); @@ -291,9 +293,70 @@ protected function getExceptionHandler() * Clear the local application instance from memory. * * @return void + * + * @deprecated This method will be removed in a future Laravel version. */ public static function forgetApp() { static::$app = null; } + + /** + * Flush the bootstrapper's global state. + * + * @return void + */ + public static function flushState() + { + if (is_null(static::$app)) { + return; + } + + static::flushHandlersState(); + + static::$app = null; + + static::$reservedMemory = null; + } + + /** + * Flush the bootstrapper's global handlers state. + * + * @return void + */ + public static function flushHandlersState() + { + while (true) { + $previousHandler = set_exception_handler(static fn () => null); + + restore_exception_handler(); + + if ($previousHandler === null) { + break; + } + + restore_exception_handler(); + } + + while (true) { + $previousHandler = set_error_handler(static fn () => null); + + restore_error_handler(); + + if ($previousHandler === null) { + break; + } + + restore_error_handler(); + } + + if (class_exists(ErrorHandler::class)) { + $instance = ErrorHandler::instance(); + + if ((fn () => $this->enabled ?? false)->call($instance)) { + $instance->disable(); + $instance->enable(); + } + } + } } diff --git a/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php b/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php index ddad0ffabcc5..700651913caf 100644 --- a/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php +++ b/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php @@ -79,4 +79,16 @@ public static function merge(array $providers, ?string $bootstrapProviderPath = array_merge(static::$merge, $providers) ))); } + + /** + * Flush the bootstrapper's global state. + * + * @return void + */ + public static function flushState() + { + static::$bootstrapProviderPath = null; + + static::$merge = []; + } } diff --git a/src/Illuminate/Foundation/Bus/PendingChain.php b/src/Illuminate/Foundation/Bus/PendingChain.php index 8d3c6892615a..2fb14990c56a 100644 --- a/src/Illuminate/Foundation/Bus/PendingChain.php +++ b/src/Illuminate/Foundation/Bus/PendingChain.php @@ -132,7 +132,7 @@ public function catchCallbacks() } /** - * Dispatch the job with the given arguments. + * Dispatch the job chain. * * @return \Illuminate\Foundation\Bus\PendingDispatch */ @@ -165,4 +165,26 @@ public function dispatch() return app(Dispatcher::class)->dispatch($firstJob); } + + /** + * Dispatch the job chain if the given truth test passes. + * + * @param bool|\Closure $boolean + * @return \Illuminate\Foundation\Bus\PendingDispatch|null + */ + public function dispatchIf($boolean) + { + return value($boolean) ? $this->dispatch() : null; + } + + /** + * Dispatch the job chain unless the given truth test passes. + * + * @param bool|\Closure $boolean + * @return \Illuminate\Foundation\Bus\PendingDispatch|null + */ + public function dispatchUnless($boolean) + { + return ! value($boolean) ? $this->dispatch() : null; + } } diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index bfefaeda3bbe..4af83acda162 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -7,14 +7,24 @@ use Illuminate\Contracts\Http\Kernel as HttpKernel; use Illuminate\Foundation\Application; use Illuminate\Foundation\Bootstrap\RegisterProviders; +use Illuminate\Foundation\Events\DiagnosingHealth; use Illuminate\Foundation\Support\Providers\EventServiceProvider as AppEventServiceProvider; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as AppRouteServiceProvider; use Illuminate\Support\Facades\Broadcast; +use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Route; +use Illuminate\Support\Facades\View; use Laravel\Folio\Folio; class ApplicationBuilder { + /** + * The service provider that are marked for registration. + * + * @var array + */ + protected array $pendingProviders = []; + /** * The Folio / page middleware that have been defined by the user. * @@ -71,13 +81,22 @@ public function withProviders(array $providers = [], bool $withBootstrapProvider /** * Register the core event service provider for the application. * + * @param array $discover * @return $this */ - public function withEvents() + public function withEvents(array $discover = []) { - $this->app->booting(function () { - $this->app->register(AppEventServiceProvider::class); - }); + if (count($discover) > 0) { + AppEventServiceProvider::setEventDiscoveryPaths($discover); + } + + if (! isset($this->pendingProviders[AppEventServiceProvider::class])) { + $this->app->booting(function () { + $this->app->register(AppEventServiceProvider::class); + }); + } + + $this->pendingProviders[AppEventServiceProvider::class] = true; return $this; } @@ -120,17 +139,18 @@ public function withRouting(?Closure $using = null, ?string $commands = null, ?string $channels = null, ?string $pages = null, + ?string $health = null, string $apiPrefix = 'api', ?callable $then = null) { - if (is_null($using) && (is_string($web) || is_string($api))) { - $using = $this->buildRoutingCallback($web, $api, $pages, $apiPrefix, $then); + if (is_null($using) && (is_string($web) || is_string($api) || is_string($pages) || is_string($health)) || is_callable($then)) { + $using = $this->buildRoutingCallback($web, $api, $pages, $health, $apiPrefix, $then); } AppRouteServiceProvider::loadRoutesUsing($using); $this->app->booting(function () { - $this->app->register(AppRouteServiceProvider::class); + $this->app->register(AppRouteServiceProvider::class, force: true); }); if (is_string($commands) && realpath($commands) !== false) { @@ -150,6 +170,7 @@ public function withRouting(?Closure $using = null, * @param string|null $web * @param string|null $api * @param string|null $pages + * @param string|null $health * @param string $apiPrefix * @param callable|null $then * @return \Closure @@ -157,14 +178,23 @@ public function withRouting(?Closure $using = null, protected function buildRoutingCallback(?string $web, ?string $api, ?string $pages, + ?string $health, string $apiPrefix, ?callable $then) { - return function () use ($web, $api, $pages, $apiPrefix, $then) { + return function () use ($web, $api, $pages, $health, $apiPrefix, $then) { if (is_string($api) && realpath($api) !== false) { Route::middleware('api')->prefix($apiPrefix)->group($api); } + if (is_string($health)) { + Route::middleware('web')->get($health, function () { + Event::dispatch(new DiagnosingHealth); + + return View::file(__DIR__.'/../resources/health-up.blade.php'); + }); + } + if (is_string($web) && realpath($web) !== false) { Route::middleware('web')->group($web); } @@ -176,7 +206,7 @@ class_exists(Folio::class)) { } if (is_callable($then)) { - $then(); + $then($this->app); } }; } @@ -184,21 +214,27 @@ class_exists(Folio::class)) { /** * Register the global middleware, middleware groups, and middleware aliases for the application. * - * @param callable $callback + * @param callable|null $callback * @return $this */ - public function withMiddleware(callable $callback) + public function withMiddleware(?callable $callback = null) { $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) { $middleware = (new Middleware) - ->auth(redirectTo: fn () => route('login')); + ->redirectGuestsTo(fn () => route('login')); - $callback($middleware); + if (! is_null($callback)) { + $callback($middleware); + } $this->pageMiddleware = $middleware->getPageMiddleware(); $kernel->setGlobalMiddleware($middleware->getGlobalMiddleware()); $kernel->setMiddlewareGroups($middleware->getMiddlewareGroups()); $kernel->setMiddlewareAliases($middleware->getMiddlewareAliases()); + + if ($priorities = $middleware->getMiddlewarePriority()) { + $kernel->setMiddlewarePriority($priorities); + } }); return $this; @@ -298,6 +334,19 @@ public function withSingletons(array $singletons) }); } + /** + * Register a callback to be invoked when the application's service providers are registered. + * + * @param callable $callback + * @return $this + */ + public function registered(callable $callback) + { + $this->app->registered($callback); + + return $this; + } + /** * Register a callback to be invoked when the application is "booting". * diff --git a/src/Illuminate/Foundation/Configuration/Exceptions.php b/src/Illuminate/Foundation/Configuration/Exceptions.php index b38bd2a20278..499d35539b71 100644 --- a/src/Illuminate/Foundation/Configuration/Exceptions.php +++ b/src/Illuminate/Foundation/Configuration/Exceptions.php @@ -4,6 +4,7 @@ use Closure; use Illuminate\Foundation\Exceptions\Handler; +use Illuminate\Support\Arr; class Exceptions { @@ -77,7 +78,7 @@ public function map($from, $to = null) * @param \Psr\Log\LogLevel::* $level * @return $this */ - public function level($type, $level) + public function level(string $type, string $level) { $this->handler->level($type, $level); @@ -100,12 +101,14 @@ public function context(Closure $contextCallback) /** * Indicate that the given exception type should not be reported. * - * @param string $class + * @param array|string $class * @return $this */ - public function dontReport(string $class) + public function dontReport(array|string $class) { - $this->handler->dontReport($class); + foreach (Arr::wrap($class) as $exceptionClass) { + $this->handler->dontReport($exceptionClass); + } return $this; } @@ -128,10 +131,23 @@ public function dontReportDuplicates() * @param array|string $attributes * @return $this */ - public function dontFlash($attributes) + public function dontFlash(array|string $attributes) { $this->handler->dontFlash($attributes); return $this; } + + /** + * Indicate that the given exception class should not be ignored. + * + * @param array>|class-string<\Throwable> $class + * @return $this + */ + public function stopIgnoring(array|string $class) + { + $this->handler->stopIgnoring($class); + + return $this; + } } diff --git a/src/Illuminate/Foundation/Configuration/Middleware.php b/src/Illuminate/Foundation/Configuration/Middleware.php index 949dfe4b5806..ff4874c9ec22 100644 --- a/src/Illuminate/Foundation/Configuration/Middleware.php +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -2,9 +2,18 @@ namespace Illuminate\Foundation\Configuration; +use Closure; use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\Middleware\Authenticate; use Illuminate\Auth\Middleware\RedirectIfAuthenticated; +use Illuminate\Cookie\Middleware\EncryptCookies; +use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; +use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance; +use Illuminate\Foundation\Http\Middleware\TrimStrings; +use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken; +use Illuminate\Http\Middleware\TrustHosts; +use Illuminate\Http\Middleware\TrustProxies; +use Illuminate\Routing\Middleware\ValidateSignature; use Illuminate\Session\Middleware\AuthenticateSession; use Illuminate\Support\Arr; @@ -15,7 +24,7 @@ class Middleware * * @var array */ - protected $global; + protected $global = []; /** * The middleware that should be prepended to the global middleware stack. @@ -111,29 +120,16 @@ class Middleware /** * Indicates if Redis throttling should be applied. * - * @var array + * @var bool */ protected $throttleWithRedis = false; /** - * The default middleware aliases. + * Indicates if sessions should be authenticated for the "web" middleware group. * - * @var array + * @var bool */ - protected $aliases = [ - 'auth' => \Illuminate\Auth\Middleware\Authenticate::class, - 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, - 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, - 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, - 'guest' => \Illuminate\Auth\Middleware\RedirectIfAuthenticated::class, - 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, - 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, - 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, - 'subscribed' => \Spark\Http\Middleware\VerifyBillableIsSubscribed::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - ]; + protected $authenticatedSessions = false; /** * The custom middleware aliases. @@ -142,6 +138,13 @@ class Middleware */ protected $customAliases = []; + /** + * The custom middleware priority definition. + * + * @var array + */ + protected $priority = []; + /** * Prepend middleware to the application's global middleware stack. * @@ -258,8 +261,8 @@ public function prependToGroup(string $group, array|string $middleware) public function appendToGroup(string $group, array|string $middleware) { $this->groupAppends[$group] = array_merge( - Arr::wrap($middleware), - $this->groupAppends[$group] ?? [] + $this->groupAppends[$group] ?? [], + Arr::wrap($middleware) ); return $this; @@ -386,6 +389,19 @@ public function alias(array $aliases) return $this; } + /** + * Define the middleware priority for the application. + * + * @param array $priority + * @return $this + */ + public function priority(array $priority) + { + $this->priority = $priority; + + return $this; + } + /** * Get the global middleware. * @@ -425,14 +441,15 @@ public function getGlobalMiddleware() public function getMiddlewareGroups() { $middleware = [ - 'web' => [ + 'web' => array_values(array_filter([ \Illuminate\Cookie\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, - ], + $this->authenticatedSessions ? 'auth.session' : null, + ])), 'api' => array_values(array_filter([ $this->statefulApi ? \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class : null, @@ -473,29 +490,106 @@ public function getMiddlewareGroups() } /** - * Configure the behavior of the authentication middleware. + * Configure where guests are redirected by the authentication middleware. + * + * @param callable|string $redirect + * @return $this + */ + public function redirectGuestsTo(callable|string $redirect) + { + return $this->redirectTo(guests: $redirect); + } + + /** + * Configure where users are redirected by the authentication and guest middleware. * - * @param callable $redirectTo + * @param callable|string $guests + * @param callable|string $users * @return $this */ - public function auth(callable $redirectTo) + public function redirectTo(callable|string $guests = null, callable|string $users = null) { - Authenticate::redirectUsing($redirectTo); - AuthenticateSession::redirectUsing($redirectTo); - AuthenticationException::redirectUsing($redirectTo); + $guests = is_string($guests) ? fn () => $guests : $guests; + $users = is_string($users) ? fn () => $users : $users; + + if ($guests) { + Authenticate::redirectUsing($guests); + AuthenticateSession::redirectUsing($guests); + AuthenticationException::redirectUsing($guests); + } + + if ($users) { + RedirectIfAuthenticated::redirectUsing($users); + } + + return $this; + } + + /** + * Configure the cookie encryption middleware. + * + * @param array $except + * @return $this + */ + public function encryptCookies(array $except = []) + { + EncryptCookies::except($except); return $this; } /** - * Configure the behavior of the "guest" middleware. + * Configure the CSRF token validation middleware. * - * @param callable $redirectTo + * @param array $except * @return $this */ - public function guest(callable $redirectTo) + public function validateCsrfTokens(array $except = []) { - RedirectIfAuthenticated::redirectUsing($redirectTo); + ValidateCsrfToken::except($except); + + return $this; + } + + /** + * Configure the URL signature validation middleware. + * + * @param array $except + * @return $this + */ + public function validateSignatures(array $except = []) + { + ValidateSignature::except($except); + + return $this; + } + + /** + * Configure the empty string conversion middleware. + * + * @param array $except + * @return $this + */ + public function convertEmptyStringsToNull(array $except = []) + { + collect($except)->each(fn (Closure $callback) => ConvertEmptyStringsToNull::skipWhen($callback)); + + return $this; + } + + /** + * Configure the string trimming middleware. + * + * @param array $except + * @return $this + */ + public function trimStrings(array $except = []) + { + [$skipWhen, $except] = collect($except)->partition(fn ($value) => $value instanceof Closure); + + $skipWhen->each(fn (Closure $callback) => TrimStrings::skipWhen($callback)); + + TrimStrings::except($except->all()); return $this; } @@ -503,12 +597,51 @@ public function guest(callable $redirectTo) /** * Indicate that the trusted host middleware should be enabled. * + * @param array|null $at + * @param bool $subdomains * @return $this */ - public function withTrustedHosts() + public function trustHosts(array $at = null, bool $subdomains = true) { $this->trustHosts = true; + if (is_array($at)) { + TrustHosts::at($at, $subdomains); + } + + return $this; + } + + /** + * Configure the trusted proxies for the application. + * + * @param array|string|null $at + * @param int|null $headers + * @return $this + */ + public function trustProxies(array|string $at = null, int $headers = null) + { + if (! is_null($at)) { + TrustProxies::at($at); + } + + if (! is_null($headers)) { + TrustProxies::withHeaders($headers); + } + + return $this; + } + + /** + * Configure the middleware that prevents requests during maintenance mode. + * + * @param array $except + * @return $this + */ + public function preventRequestsDuringMaintenance(array $except = []) + { + PreventRequestsDuringMaintenance::except($except); + return $this; } @@ -517,7 +650,7 @@ public function withTrustedHosts() * * @return $this */ - public function withStatefulApi() + public function statefulApi() { $this->statefulApi = true; @@ -531,7 +664,7 @@ public function withStatefulApi() * @param bool $redis * @return $this */ - public function withThrottledApi($limiter = 'api', $redis = false) + public function throttleApi($limiter = 'api', $redis = false) { $this->apiLimiter = $limiter; @@ -554,6 +687,18 @@ public function throttleWithRedis() return $this; } + /** + * Indicate that sessions should be authenticated for the "web" middleware group. + * + * @return $this + */ + public function authenticateSessions() + { + $this->authenticatedSessions = true; + + return $this; + } + /** * Get the Folio / page middleware for the application. * @@ -598,4 +743,14 @@ protected function defaultAliases() 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; } + + /** + * Get the middleware priority for the application. + * + * @return array + */ + public function getMiddlewarePriority() + { + return $this->priority; + } } diff --git a/src/Illuminate/Foundation/Console/AboutCommand.php b/src/Illuminate/Foundation/Console/AboutCommand.php index 23d2b23b631a..20c24e8e7c11 100644 --- a/src/Illuminate/Foundation/Console/AboutCommand.php +++ b/src/Illuminate/Foundation/Console/AboutCommand.php @@ -159,6 +159,8 @@ protected function displayJson($data) */ protected function gatherApplicationInformation() { + self::$data = []; + $formatEnabledStatus = fn ($value) => $value ? 'ENABLED' : 'OFF'; $formatCachedStatus = fn ($value) => $value ? 'CACHED' : 'NOT CACHED'; @@ -300,4 +302,16 @@ protected function toSearchKeyword(string $value) { return (string) Str::of($value)->lower()->snake(); } + + /** + * Flush the registered about data. + * + * @return void + */ + public static function flushState() + { + static::$data = []; + + static::$customDataResolvers = []; + } } diff --git a/src/Illuminate/Foundation/Console/ApiInstallCommand.php b/src/Illuminate/Foundation/Console/ApiInstallCommand.php index 2a7a072a0461..9bab2abda4c4 100644 --- a/src/Illuminate/Foundation/Console/ApiInstallCommand.php +++ b/src/Illuminate/Foundation/Console/ApiInstallCommand.php @@ -90,7 +90,7 @@ protected function uncommentApiRoutesFile() protected function installSanctum() { $this->requireComposerPackages($this->option('composer'), [ - 'laravel/sanctum:dev-master', + 'laravel/sanctum:^4.0', ]); $php = (new PhpExecutableFinder())->find(false) ?: 'php'; diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php index d0b606e4e74e..e62426b99f12 100644 --- a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -2,19 +2,27 @@ namespace Illuminate\Foundation\Console; +use Composer\InstalledVersions; use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Facades\Process; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Process\PhpExecutableFinder; + +use function Laravel\Prompts\confirm; #[AsCommand(name: 'install:broadcasting')] class BroadcastingInstallCommand extends Command { + use InteractsWithComposerPackages; + /** * The name and signature of the console command. * * @var string */ protected $signature = 'install:broadcasting + {--composer=global : Absolute path to the Composer binary which should be used to install packages} {--force : Overwrite any existing broadcasting routes file}'; /** @@ -31,6 +39,8 @@ class BroadcastingInstallCommand extends Command */ public function handle() { + $this->call('config:publish', ['name' => 'broadcasting']); + // Install channel routes file... if (file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) && ! $this->option('force')) { @@ -56,10 +66,12 @@ public function handle() if (! str_contains($bootstrapScript, 'echo.js')) { file_put_contents( $bootstrapScriptPath, - $bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub') + trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, ); } } + + $this->installReverb(); } /** @@ -91,4 +103,36 @@ protected function uncommentChannelsRoutesFile() return; } } + + /** + * Install Laravel Reverb into the application if desired. + * + * @return void + */ + protected function installReverb() + { + if (InstalledVersions::isInstalled('laravel/reverb')) { + return; + } + + $install = confirm('Would you like to install Laravel Reverb?', default: true); + + if (! $install) { + return; + } + + $this->requireComposerPackages($this->option('composer'), [ + 'laravel/reverb:@beta', + ]); + + $php = (new PhpExecutableFinder())->find(false) ?: 'php'; + + Process::run([ + $php, + defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan', + 'reverb:install', + ]); + + $this->components->info('Reverb installed successfully.'); + } } diff --git a/src/Illuminate/Foundation/Console/ClassMakeCommand.php b/src/Illuminate/Foundation/Console/ClassMakeCommand.php new file mode 100644 index 000000000000..a5d32b84089d --- /dev/null +++ b/src/Illuminate/Foundation/Console/ClassMakeCommand.php @@ -0,0 +1,70 @@ +option('invokable')) { + return __DIR__.'/stubs/class.invokable.stub'; + } + + return __DIR__.'/stubs/class.stub'; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return $rootNamespace; + } + + /** + * Get the console command arguments. + * + * @return array + */ + protected function getOptions() + { + return [ + ['invokable', 'i', InputOption::VALUE_NONE, 'Generate a single method, invokable class'], + ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the class already exists'], + ]; + } +} diff --git a/src/Illuminate/Foundation/Console/CliDumper.php b/src/Illuminate/Foundation/Console/CliDumper.php index 304dfcb0c351..6f5fd9a49886 100644 --- a/src/Illuminate/Foundation/Console/CliDumper.php +++ b/src/Illuminate/Foundation/Console/CliDumper.php @@ -57,6 +57,8 @@ public function __construct($output, $basePath, $compiledViewPath) $this->basePath = $basePath; $this->output = $output; $this->compiledViewPath = $compiledViewPath; + + $this->setColors($this->supportsColors()); } /** diff --git a/src/Illuminate/Foundation/Console/ConfigPublishCommand.php b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php index bc12f35cd4d8..54ebce46eb94 100644 --- a/src/Illuminate/Foundation/Console/ConfigPublishCommand.php +++ b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php @@ -92,7 +92,9 @@ protected function getBaseConfigurationFiles() $config = []; foreach (Finder::create()->files()->name('*.php')->in(__DIR__.'/../../../../config') as $file) { - $config[basename($file->getRealPath(), '.php')] = $file->getRealPath(); + $name = basename($file->getRealPath(), '.php'); + + $config[$name] = file_exists($stubPath = (__DIR__.'/../../../../config-stubs/'.$name.'.php')) ? $stubPath : $file->getRealPath(); } return collect($config)->sortKeys()->all(); diff --git a/src/Illuminate/Foundation/Console/EnumMakeCommand.php b/src/Illuminate/Foundation/Console/EnumMakeCommand.php new file mode 100644 index 000000000000..eeb97d40f142 --- /dev/null +++ b/src/Illuminate/Foundation/Console/EnumMakeCommand.php @@ -0,0 +1,109 @@ +option('string') || $this->option('int')) { + return __DIR__.'/stubs/enum.backed.stub'; + } + + return __DIR__.'/stubs/enum.stub'; + } + + /** + * Build the class with the given name. + * + * @param string $name + * @return string + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + protected function buildClass($name) + { + if ($this->option('string') || $this->option('int')) { + return str_replace( + ['{{ type }}'], + $this->option('string') ? 'string' : 'int', + parent::buildClass($name) + ); + } + + return parent::buildClass($name); + } + + /** + * Interact further with the user if they were prompted for missing arguments. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) + { + if ($this->didReceiveOptions($input)) { + return; + } + + $type = select('Which type of enum would you like?', [ + 'pure' => 'Pure enum', + 'string' => 'Backed enum (String)', + 'int' => 'Backed enum (Integer)', + ]); + + if ($type !== 'pure') { + $input->setOption($type, true); + } + } + + /** + * Get the console command arguments. + * + * @return array + */ + protected function getOptions() + { + return [ + ['string', 's', InputOption::VALUE_NONE, 'Generate a string backed enum.'], + ['int', 'i', InputOption::VALUE_NONE, 'Generate an integer backed enum.'], + ['force', 'f', InputOption::VALUE_NONE, 'Create the enum even if the enum already exists'], + ]; + } +} diff --git a/src/Illuminate/Foundation/Console/EventGenerateCommand.php b/src/Illuminate/Foundation/Console/EventGenerateCommand.php index 5b0857a3a11a..ad99e9373357 100644 --- a/src/Illuminate/Foundation/Console/EventGenerateCommand.php +++ b/src/Illuminate/Foundation/Console/EventGenerateCommand.php @@ -23,6 +23,13 @@ class EventGenerateCommand extends Command */ protected $description = 'Generate the missing events and listeners based on registration'; + /** + * Indicates whether the command should be shown in the Artisan command list. + * + * @var bool + */ + protected $hidden = true; + /** * Execute the console command. * diff --git a/src/Illuminate/Foundation/Console/ExceptionMakeCommand.php b/src/Illuminate/Foundation/Console/ExceptionMakeCommand.php index 7cf7faaf43c0..bcd82ab0c57d 100644 --- a/src/Illuminate/Foundation/Console/ExceptionMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ExceptionMakeCommand.php @@ -4,7 +4,11 @@ use Illuminate\Console\GeneratorCommand; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; + +use function Laravel\Prompts\{confirm}; #[AsCommand(name: 'make:exception')] class ExceptionMakeCommand extends GeneratorCommand @@ -70,6 +74,23 @@ protected function getDefaultNamespace($rootNamespace) return $rootNamespace.'\Exceptions'; } + /** + * Interact further with the user if they were prompted for missing arguments. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @return void + */ + protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) + { + if ($this->didReceiveOptions($input)) { + return; + } + + $input->setOption('report', confirm('Should the exception have a report method?', default: false)); + $input->setOption('render', confirm('Should the exception have a render method?', default: false)); + } + /** * Get the console command options. * diff --git a/src/Illuminate/Foundation/Console/InterfaceMakeCommand.php b/src/Illuminate/Foundation/Console/InterfaceMakeCommand.php new file mode 100644 index 000000000000..e30103497bb0 --- /dev/null +++ b/src/Illuminate/Foundation/Console/InterfaceMakeCommand.php @@ -0,0 +1,65 @@ +app['config']->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER')); + return $this->app['config']->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER', function () { + return Env::get('SCHEDULE_CACHE_STORE'); + })); } /** @@ -361,7 +363,7 @@ protected function load($paths) $namespace = $this->app->getNamespace(); - foreach ((new Finder)->in($paths)->files() as $file) { + foreach (Finder::create()->in($paths)->files() as $file) { $command = $this->commandClassFromFile($file, $namespace); if (is_subclass_of($command, Command::class) && diff --git a/src/Illuminate/Foundation/Console/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index 194c80fa5127..a73bf52ef5c1 100644 --- a/src/Illuminate/Foundation/Console/ServeCommand.php +++ b/src/Illuminate/Foundation/Console/ServeCommand.php @@ -57,6 +57,9 @@ class ServeCommand extends Command */ public static $passthroughVariables = [ 'APP_ENV', + 'HERD_PHP_81_INI_SCAN_DIR', + 'HERD_PHP_82_INI_SCAN_DIR', + 'HERD_PHP_83_INI_SCAN_DIR', 'IGNITION_LOCAL_SITES_PATH', 'LARAVEL_SAIL', 'PATH', @@ -140,6 +143,14 @@ protected function startProcess($hasEnvironment) return in_array($key, static::$passthroughVariables) ? [$key => $value] : [$key => false]; })->all()); + $this->trap(fn () => [SIGTERM, SIGINT, SIGHUP, SIGUSR1, SIGUSR2, SIGQUIT], function ($signal) use ($process) { + if ($process->isRunning()) { + $process->stop(10, $signal); + } + + exit; + }); + $process->start($this->handleProcessOutput()); return $process; diff --git a/src/Illuminate/Foundation/Console/StorageUnlinkCommand.php b/src/Illuminate/Foundation/Console/StorageUnlinkCommand.php new file mode 100644 index 000000000000..504bc80ab0ac --- /dev/null +++ b/src/Illuminate/Foundation/Console/StorageUnlinkCommand.php @@ -0,0 +1,53 @@ +links() as $link => $target) { + if (! file_exists($link) || ! is_link($link)) { + continue; + } + + $this->laravel->make('files')->delete($link); + + $this->components->info("The [$link] link has been deleted."); + } + } + + /** + * Get the symbolic links that are configured for the application. + * + * @return array + */ + protected function links() + { + return $this->laravel['config']['filesystems.links'] ?? + [public_path('storage') => storage_path('app/public')]; + } +} diff --git a/src/Illuminate/Foundation/Console/StubPublishCommand.php b/src/Illuminate/Foundation/Console/StubPublishCommand.php index e2a293d489db..aa0f423f79e1 100644 --- a/src/Illuminate/Foundation/Console/StubPublishCommand.php +++ b/src/Illuminate/Foundation/Console/StubPublishCommand.php @@ -40,7 +40,11 @@ public function handle() $stubs = [ __DIR__.'/stubs/cast.inbound.stub' => 'cast.inbound.stub', __DIR__.'/stubs/cast.stub' => 'cast.stub', + __DIR__.'/stubs/class.stub' => 'class.stub', + __DIR__.'/stubs/class.invokable.stub' => 'class.invokable.stub', __DIR__.'/stubs/console.stub' => 'console.stub', + __DIR__.'/stubs/enum.stub' => 'enum.stub', + __DIR__.'/stubs/enum.backed.stub' => 'enum.backed.stub', __DIR__.'/stubs/event.stub' => 'event.stub', __DIR__.'/stubs/job.queued.stub' => 'job.queued.stub', __DIR__.'/stubs/job.stub' => 'job.stub', @@ -62,6 +66,7 @@ public function handle() __DIR__.'/stubs/scope.stub' => 'scope.stub', __DIR__.'/stubs/test.stub' => 'test.stub', __DIR__.'/stubs/test.unit.stub' => 'test.unit.stub', + __DIR__.'/stubs/trait.stub' => 'trait.stub', __DIR__.'/stubs/view-component.stub' => 'view-component.stub', realpath(__DIR__.'/../../Database/Console/Factories/stubs/factory.stub') => 'factory.stub', realpath(__DIR__.'/../../Database/Console/Seeds/stubs/seeder.stub') => 'seeder.stub', diff --git a/src/Illuminate/Foundation/Console/TestMakeCommand.php b/src/Illuminate/Foundation/Console/TestMakeCommand.php index 7b65cf991366..85440589f52d 100644 --- a/src/Illuminate/Foundation/Console/TestMakeCommand.php +++ b/src/Illuminate/Foundation/Console/TestMakeCommand.php @@ -44,7 +44,7 @@ protected function getStub() { $suffix = $this->option('unit') ? '.unit.stub' : '.stub'; - return $this->option('pest') + return $this->usingPest() ? $this->resolveStubPath('/stubs/pest'.$suffix) : $this->resolveStubPath('/stubs/test'.$suffix); } @@ -108,9 +108,10 @@ protected function rootNamespace() protected function getOptions() { return [ - ['force', 'f', InputOption::VALUE_NONE, 'Create the class even if the test already exists'], + ['force', 'f', InputOption::VALUE_NONE, 'Create the test even if the test already exists'], ['unit', 'u', InputOption::VALUE_NONE, 'Create a unit test'], - ['pest', 'p', InputOption::VALUE_NONE, 'Create a Pest test'], + ['pest', null, InputOption::VALUE_NONE, 'Create a Pest test'], + ['phpunit', null, InputOption::VALUE_NONE, 'Create a PHPUnit test'], ]; } @@ -128,17 +129,29 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp } $type = select('Which type of test would you like?', [ - 'feature' => 'Feature (PHPUnit)', - 'unit' => 'Unit (PHPUnit)', - 'pest-feature' => 'Feature (Pest)', - 'pest-unit' => 'Unit (Pest)', + 'feature' => 'Feature', + 'unit' => 'Unit', ]); match ($type) { 'feature' => null, 'unit' => $input->setOption('unit', true), - 'pest-feature' => $input->setOption('pest', true), - 'pest-unit' => tap($input)->setOption('pest', true)->setOption('unit', true), }; } + + /** + * Determine if Pest is being used by the application. + * + * @return bool + */ + protected function usingPest() + { + if ($this->option('phpunit')) { + return false; + } + + return $this->option('pest') || + (function_exists('\Pest\\version') && + file_exists(base_path('tests').'/Pest.php')); + } } diff --git a/src/Illuminate/Foundation/Console/TraitMakeCommand.php b/src/Illuminate/Foundation/Console/TraitMakeCommand.php new file mode 100644 index 000000000000..6f1c3172e06e --- /dev/null +++ b/src/Illuminate/Foundation/Console/TraitMakeCommand.php @@ -0,0 +1,65 @@ +option('test') && ! $this->option('pest')) { + if (! $this->option('test') && ! $this->option('pest') && ! $this->option('phpunit')) { return false; } @@ -201,7 +201,7 @@ protected function testClassFullyQualifiedName() */ protected function getTestStub() { - $stubName = 'view.'.($this->option('pest') ? 'pest' : 'test').'.stub'; + $stubName = 'view.'.($this->usingPest() ? 'pest' : 'test').'.stub'; return file_exists($customPath = $this->laravel->basePath("stubs/$stubName")) ? $customPath @@ -221,6 +221,22 @@ protected function testViewName() ->value(); } + /** + * Determine if Pest is being used by the application. + * + * @return bool + */ + protected function usingPest() + { + if ($this->option('phpunit')) { + return false; + } + + return $this->option('pest') || + (function_exists('\Pest\\version') && + file_exists(base_path('tests').'/Pest.php')); + } + /** * Get the console command arguments. * diff --git a/src/Illuminate/Foundation/Console/stubs/api-routes.stub b/src/Illuminate/Foundation/Console/stubs/api-routes.stub index aee5f3636395..0e7503c70253 100644 --- a/src/Illuminate/Foundation/Console/stubs/api-routes.stub +++ b/src/Illuminate/Foundation/Console/stubs/api-routes.stub @@ -4,17 +4,6 @@ use Illuminate\Auth\Middleware\Authenticate; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; -/* -|-------------------------------------------------------------------------- -| API Routes -|-------------------------------------------------------------------------- -| -| Here is where you can register API routes for your application. These -| routes are loaded within the "api" middleware group which includes -| the middleware most often needed by APIs. Build something great! -| -*/ - Route::get('/user', function (Request $request) { return $request->user(); })->middleware(Authenticate::using('sanctum')); diff --git a/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub b/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub index 5d451e1fae88..df2ad287ec14 100644 --- a/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub +++ b/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub @@ -2,17 +2,6 @@ use Illuminate\Support\Facades\Broadcast; -/* -|-------------------------------------------------------------------------- -| Broadcast Channels -|-------------------------------------------------------------------------- -| -| Here you may register all of the event broadcasting channels that your -| application supports. The given channel authorization callbacks are -| used to check if an authenticated user can listen to the channel. -| -*/ - Broadcast::channel('App.Models.User.{id}', function ($user, $id) { return (int) $user->id === (int) $id; }); diff --git a/src/Illuminate/Foundation/Console/stubs/class.invokable.stub b/src/Illuminate/Foundation/Console/stubs/class.invokable.stub new file mode 100644 index 000000000000..c55610cfe4a6 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/class.invokable.stub @@ -0,0 +1,22 @@ +setCompiledRoutes( {{routes}} ); diff --git a/src/Illuminate/Foundation/Console/stubs/trait.stub b/src/Illuminate/Foundation/Console/stubs/trait.stub new file mode 100644 index 000000000000..e40984771f42 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/trait.stub @@ -0,0 +1,8 @@ +files()->in($listenerPath), $basePath + Finder::create()->files()->in($listenerPath), $basePath )); $discoveredEvents = []; diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index facb54fe54c1..ff3231ef9cb7 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -251,23 +251,25 @@ public function map($from, $to = null) * * Alias of "ignore". * - * @param string $class + * @param array|string $exceptions * @return $this */ - public function dontReport(string $class) + public function dontReport(array|string $exceptions) { - return $this->ignore($class); + return $this->ignore($exceptions); } /** * Indicate that the given exception type should not be reported. * - * @param string $class + * @param array|string $class * @return $this */ - public function ignore(string $class) + public function ignore(array|string $exceptions) { - $this->dontReport[] = $class; + $exceptions = Arr::wrap($exceptions); + + $this->dontReport = array_values(array_unique(array_merge($this->dontReport, $exceptions))); return $this; } @@ -278,7 +280,7 @@ public function ignore(string $class) * @param array|string $attributes * @return $this */ - public function dontFlash($attributes) + public function dontFlash(array|string $attributes) { $this->dontFlash = array_values(array_unique( array_merge($this->dontFlash, Arr::wrap($attributes)) @@ -450,16 +452,18 @@ public function throttleUsing(callable $throttleUsing) /** * Remove the given exception class from the list of exceptions that should be ignored. * - * @param string $exception + * @param array|string $exceptions * @return $this */ - public function stopIgnoring(string $exception) + public function stopIgnoring(array|string $exceptions) { + $exceptions = Arr::wrap($exceptions); + $this->dontReport = collect($this->dontReport) - ->reject(fn ($ignored) => $ignored === $exception)->values()->all(); + ->reject(fn ($ignored) => in_array($ignored, $exceptions))->values()->all(); $this->internalDontReport = collect($this->internalDontReport) - ->reject(fn ($ignored) => $ignored === $exception)->values()->all(); + ->reject(fn ($ignored) => in_array($ignored, $exceptions))->values()->all(); return $this; } @@ -933,7 +937,7 @@ public function renderForConsole($output, Throwable $e) $message .= '. Did you mean one of these?'; with(new Error($output))->render($message); - with(new BulletList($output))->render($e->getAlternatives()); + with(new BulletList($output))->render($alternatives); $output->writeln(''); } else { diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 772a2635a3b4..4824c27c1799 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -115,11 +115,13 @@ protected function getValidatorInstance() */ protected function createDefaultValidator(ValidationFactory $factory) { - $rules = method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : []; + $rules = $this->validationRules(); $validator = $factory->make( - $this->validationData(), $rules, - $this->messages(), $this->attributes() + $this->validationData(), + $rules, + $this->messages(), + $this->attributes(), )->stopOnFirstFailure($this->stopOnFirstFailure); if ($this->isPrecognitive()) { @@ -141,6 +143,16 @@ public function validationData() return $this->all(); } + /** + * Get the validation rules for this form request. + * + * @return array + */ + protected function validationRules() + { + return method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : []; + } + /** * Handle a failed validation attempt. * diff --git a/src/Illuminate/Foundation/Http/Kernel.php b/src/Illuminate/Foundation/Http/Kernel.php index 2ddb1c58c8bf..79d5a6d5336f 100644 --- a/src/Illuminate/Foundation/Http/Kernel.php +++ b/src/Illuminate/Foundation/Http/Kernel.php @@ -596,6 +596,21 @@ public function setMiddlewareAliases(array $aliases) return $this; } + /** + * Set the application's middleware priority. + * + * @param array $priority + * @return $this + */ + public function setMiddlewarePriority(array $priority) + { + $this->middlewarePriority = $priority; + + $this->syncMiddlewareToRouter(); + + return $this; + } + /** * Get the Laravel application instance. * diff --git a/src/Illuminate/Foundation/Http/Middleware/Concerns/ExcludesPaths.php b/src/Illuminate/Foundation/Http/Middleware/Concerns/ExcludesPaths.php new file mode 100644 index 000000000000..622d9dd0b5da --- /dev/null +++ b/src/Illuminate/Foundation/Http/Middleware/Concerns/ExcludesPaths.php @@ -0,0 +1,44 @@ + + */ + protected $except = []; + + /** + * Determine if the request has a URI that should be excluded. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + protected function inExceptArray($request) + { + foreach ($this->getExcludedPaths() as $except) { + if ($except !== '/') { + $except = trim($except, '/'); + } + + if ($request->fullUrlIs($except) || $request->is($except)) { + return true; + } + } + + return false; + } + + /** + * Get the URIs that should be excluded. + * + * @return array + */ + public function getExcludedPaths() + { + return $this->except; + } +} diff --git a/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php b/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php index a25e4c4a4b91..0ecfa38598e5 100644 --- a/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php +++ b/src/Illuminate/Foundation/Http/Middleware/PreventRequestsDuringMaintenance.php @@ -6,10 +6,14 @@ use ErrorException; use Illuminate\Contracts\Foundation\Application; use Illuminate\Foundation\Http\MaintenanceModeBypassCookie; +use Illuminate\Foundation\Http\Middleware\Concerns\ExcludesPaths; +use Illuminate\Support\Arr; use Symfony\Component\HttpKernel\Exception\HttpException; class PreventRequestsDuringMaintenance { + use ExcludesPaths; + /** * The application implementation. * @@ -18,11 +22,11 @@ class PreventRequestsDuringMaintenance protected $app; /** - * The URIs that should be accessible while maintenance mode is enabled. + * The URIs that should be accessible during maintenance. * - * @var array + * @var array */ - protected $except = []; + protected static $neverPrevent = []; /** * Create a new middleware instance. @@ -116,27 +120,6 @@ protected function hasValidBypassCookie($request, array $data) ); } - /** - * Determine if the request has a URI that should be accessible in maintenance mode. - * - * @param \Illuminate\Http\Request $request - * @return bool - */ - protected function inExceptArray($request) - { - foreach ($this->getExcludedPaths() as $except) { - if ($except !== '/') { - $except = trim($except, '/'); - } - - if ($request->fullUrlIs($except) || $request->is($except)) { - return true; - } - } - - return false; - } - /** * Redirect the user back to the root of the application with a maintenance mode bypass cookie. * @@ -168,12 +151,35 @@ protected function getHeaders($data) } /** - * Get the URIs that should be accessible even when maintenance mode is enabled. + * Get the URIs that should be excluded. * * @return array */ public function getExcludedPaths() { - return $this->except; + return array_merge($this->except, static::$neverPrevent); + } + + /** + * Indicate that the given URIs should always be accessible. + * + * @param array|string $uris + * @return void + */ + public static function except($uris) + { + static::$neverPrevent = array_values(array_unique( + array_merge(static::$neverPrevent, Arr::wrap($uris)) + )); + } + + /** + * Flush the state of the middleware. + * + * @return void + */ + public static function flushState() + { + static::$neverPrevent = []; } } diff --git a/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php b/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php index 10d8a137d57e..f5ad195b0325 100644 --- a/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php +++ b/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php @@ -99,6 +99,8 @@ public static function skipWhen(Closure $callback) */ public static function flushState() { + static::$neverTrim = []; + static::$skipCallbacks = []; } } diff --git a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php index d39b09981a80..ba2f67e6af11 100644 --- a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php +++ b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Support\Responsable; use Illuminate\Cookie\CookieValuePrefix; use Illuminate\Cookie\Middleware\EncryptCookies; +use Illuminate\Foundation\Http\Middleware\Concerns\ExcludesPaths; use Illuminate\Session\TokenMismatchException; use Illuminate\Support\Arr; use Illuminate\Support\InteractsWithTime; @@ -16,7 +17,8 @@ class VerifyCsrfToken { - use InteractsWithTime; + use InteractsWithTime, + ExcludesPaths; /** * The application instance. @@ -32,13 +34,6 @@ class VerifyCsrfToken */ protected $encrypter; - /** - * The URIs that should be excluded from CSRF verification. - * - * @var array - */ - protected $except = []; - /** * The globally ignored URIs that should be excluded from CSRF verification. * @@ -115,24 +110,13 @@ protected function runningUnitTests() } /** - * Determine if the request has a URI that should pass through CSRF verification. + * Get the URIs that should be excluded. * - * @param \Illuminate\Http\Request $request - * @return bool + * @return array */ - protected function inExceptArray($request) + public function getExcludedPaths() { - foreach (array_merge($this->except, static::$neverVerify) as $except) { - if ($except !== '/') { - $except = trim($except, '/'); - } - - if ($request->fullUrlIs($except) || $request->is($except)) { - return true; - } - } - - return false; + return array_merge($this->except, static::$neverVerify); } /** @@ -227,13 +211,13 @@ protected function newCookie($request, $config) /** * Indicate that the given URIs should be excluded from CSRF verification. * - * @param array|string $paths + * @param array|string $uris * @return void */ - public static function except($paths) + public static function except($uris) { static::$neverVerify = array_values(array_unique( - array_merge(static::$neverVerify, Arr::wrap($paths)) + array_merge(static::$neverVerify, Arr::wrap($uris)) )); } @@ -246,4 +230,14 @@ public static function serialized() { return EncryptCookies::serialized('XSRF-TOKEN'); } + + /** + * Flush the state of the middleware. + * + * @return void + */ + public static function flushState() + { + static::$neverVerify = []; + } } diff --git a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php index eab3b0d97adb..4ec8289653d7 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -33,6 +33,7 @@ use Illuminate\Foundation\Console\CastMakeCommand; use Illuminate\Foundation\Console\ChannelListCommand; use Illuminate\Foundation\Console\ChannelMakeCommand; +use Illuminate\Foundation\Console\ClassMakeCommand; use Illuminate\Foundation\Console\ClearCompiledCommand; use Illuminate\Foundation\Console\ComponentMakeCommand; use Illuminate\Foundation\Console\ConfigCacheCommand; @@ -42,6 +43,7 @@ use Illuminate\Foundation\Console\ConsoleMakeCommand; use Illuminate\Foundation\Console\DocsCommand; use Illuminate\Foundation\Console\DownCommand; +use Illuminate\Foundation\Console\EnumMakeCommand; use Illuminate\Foundation\Console\EnvironmentCommand; use Illuminate\Foundation\Console\EnvironmentDecryptCommand; use Illuminate\Foundation\Console\EnvironmentEncryptCommand; @@ -51,6 +53,7 @@ use Illuminate\Foundation\Console\EventListCommand; use Illuminate\Foundation\Console\EventMakeCommand; use Illuminate\Foundation\Console\ExceptionMakeCommand; +use Illuminate\Foundation\Console\InterfaceMakeCommand; use Illuminate\Foundation\Console\JobMakeCommand; use Illuminate\Foundation\Console\KeyGenerateCommand; use Illuminate\Foundation\Console\LangPublishCommand; @@ -73,8 +76,10 @@ use Illuminate\Foundation\Console\ScopeMakeCommand; use Illuminate\Foundation\Console\ServeCommand; use Illuminate\Foundation\Console\StorageLinkCommand; +use Illuminate\Foundation\Console\StorageUnlinkCommand; use Illuminate\Foundation\Console\StubPublishCommand; use Illuminate\Foundation\Console\TestMakeCommand; +use Illuminate\Foundation\Console\TraitMakeCommand; use Illuminate\Foundation\Console\UpCommand; use Illuminate\Foundation\Console\VendorPublishCommand; use Illuminate\Foundation\Console\ViewCacheCommand; @@ -161,6 +166,7 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'ScheduleInterrupt' => ScheduleInterruptCommand::class, 'ShowModel' => ShowModelCommand::class, 'StorageLink' => StorageLinkCommand::class, + 'StorageUnlink' => StorageUnlinkCommand::class, 'Up' => UpCommand::class, 'ViewCache' => ViewCacheCommand::class, 'ViewClear' => ViewClearCommand::class, @@ -178,15 +184,18 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'CastMake' => CastMakeCommand::class, 'ChannelList' => ChannelListCommand::class, 'ChannelMake' => ChannelMakeCommand::class, + 'ClassMake' => ClassMakeCommand::class, 'ComponentMake' => ComponentMakeCommand::class, 'ConfigPublish' => ConfigPublishCommand::class, 'ConsoleMake' => ConsoleMakeCommand::class, 'ControllerMake' => ControllerMakeCommand::class, 'Docs' => DocsCommand::class, + 'EnumMake' => EnumMakeCommand::class, 'EventGenerate' => EventGenerateCommand::class, 'EventMake' => EventMakeCommand::class, 'ExceptionMake' => ExceptionMakeCommand::class, 'FactoryMake' => FactoryMakeCommand::class, + 'InterfaceMake' => InterfaceMakeCommand::class, 'JobMake' => JobMakeCommand::class, 'LangPublish' => LangPublishCommand::class, 'ListenerMake' => ListenerMakeCommand::class, @@ -210,6 +219,7 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid 'Serve' => ServeCommand::class, 'StubPublish' => StubPublishCommand::class, 'TestMake' => TestMakeCommand::class, + 'TraitMake' => TraitMakeCommand::class, 'VendorPublish' => VendorPublishCommand::class, 'ViewMake' => ViewMakeCommand::class, ]; @@ -326,6 +336,18 @@ protected function registerChannelMakeCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerClassMakeCommand() + { + $this->app->singleton(ClassMakeCommand::class, function ($app) { + return new ClassMakeCommand($app['files']); + }); + } + /** * Register the command. * @@ -398,6 +420,18 @@ protected function registerControllerMakeCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerEnumMakeCommand() + { + $this->app->singleton(EnumMakeCommand::class, function ($app) { + return new EnumMakeCommand($app['files']); + }); + } + /** * Register the command. * @@ -446,6 +480,18 @@ protected function registerEventClearCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerInterfaceMakeCommand() + { + $this->app->singleton(InterfaceMakeCommand::class, function ($app) { + return new InterfaceMakeCommand($app['files']); + }); + } + /** * Register the command. * @@ -816,6 +862,18 @@ protected function registerTestMakeCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerTraitMakeCommand() + { + $this->app->singleton(TraitMakeCommand::class, function ($app) { + return new TraitMakeCommand($app['files']); + }); + } + /** * Register the command. * diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index 2134171e0ee7..7a9bb7c91a66 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -98,11 +98,11 @@ public function registerConsoleSchedule() */ public function registerDumper() { - AbstractCloner::$defaultCasters[ConnectionInterface::class] = [StubCaster::class, 'cutInternals']; - AbstractCloner::$defaultCasters[Container::class] = [StubCaster::class, 'cutInternals']; - AbstractCloner::$defaultCasters[Dispatcher::class] = [StubCaster::class, 'cutInternals']; - AbstractCloner::$defaultCasters[Factory::class] = [StubCaster::class, 'cutInternals']; - AbstractCloner::$defaultCasters[Grammar::class] = [StubCaster::class, 'cutInternals']; + AbstractCloner::$defaultCasters[ConnectionInterface::class] ??= [StubCaster::class, 'cutInternals']; + AbstractCloner::$defaultCasters[Container::class] ??= [StubCaster::class, 'cutInternals']; + AbstractCloner::$defaultCasters[Dispatcher::class] ??= [StubCaster::class, 'cutInternals']; + AbstractCloner::$defaultCasters[Factory::class] ??= [StubCaster::class, 'cutInternals']; + AbstractCloner::$defaultCasters[Grammar::class] ??= [StubCaster::class, 'cutInternals']; $basePath = $this->app->basePath(); diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index 6dd42b16ff9c..69f4e63f28f4 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -32,6 +32,13 @@ class EventServiceProvider extends ServiceProvider */ protected $observers = []; + /** + * The configured event discovery paths. + * + * @var array|null + */ + protected static $eventDiscoveryPaths; + /** * Register the application's event listeners. * @@ -149,11 +156,22 @@ public function discoverEvents() */ protected function discoverEventsWithin() { - return [ + return static::$eventDiscoveryPaths ?: [ $this->app->path('Listeners'), ]; } + /** + * Set the globally configured event discovery paths. + * + * @param array $paths + * @return void + */ + public static function setEventDiscoveryPaths(array $paths) + { + static::$eventDiscoveryPaths = $paths; + } + /** * Get the base path to be used during event discovery. * diff --git a/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php index 9c9ba4f90be6..323f0f32b42a 100644 --- a/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php @@ -90,7 +90,7 @@ protected function routes(Closure $routesCallback) */ public static function loadRoutesUsing(Closure $routesCallback) { - static::$alwaysLoadRoutesUsing = $routesCallback; + self::$alwaysLoadRoutesUsing = $routesCallback; } /** @@ -134,9 +134,11 @@ protected function loadCachedRoutes() */ protected function loadRoutes() { - if (! is_null(static::$alwaysLoadRoutesUsing)) { - $this->app->call(static::$alwaysLoadRoutesUsing); - } elseif (! is_null($this->loadRoutesUsing)) { + if (! is_null(self::$alwaysLoadRoutesUsing)) { + $this->app->call(self::$alwaysLoadRoutesUsing); + } + + if (! is_null($this->loadRoutesUsing)) { $this->app->call($this->loadRoutesUsing); } elseif (method_exists($this, 'map')) { $this->app->call([$this, 'map']); diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php new file mode 100644 index 000000000000..3d210cd7888d --- /dev/null +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -0,0 +1,291 @@ +app) { + $this->refreshApplication(); + + ParallelTesting::callSetUpTestCaseCallbacks($this); + } + + $this->setUpTraits(); + + foreach ($this->afterApplicationCreatedCallbacks as $callback) { + $callback(); + } + + Model::setEventDispatcher($this->app['events']); + + $this->setUpHasRun = true; + } + + /** + * Clean up the testing environment before the next test. + * + * @internal + * + * @return void + */ + protected function tearDownTheTestEnvironment(): void + { + if ($this->app) { + $this->callBeforeApplicationDestroyedCallbacks(); + + ParallelTesting::callTearDownTestCaseCallbacks($this); + + $this->app->flush(); + + $this->app = null; + } + + $this->setUpHasRun = false; + + if (property_exists($this, 'serverVariables')) { + $this->serverVariables = []; + } + + if (property_exists($this, 'defaultHeaders')) { + $this->defaultHeaders = []; + } + + if (class_exists('Mockery')) { + if ($container = Mockery::getContainer()) { + $this->addToAssertionCount($container->mockery_getExpectationCount()); + } + + try { + Mockery::close(); + } catch (InvalidCountException $e) { + if (! Str::contains($e->getMethodName(), ['doWrite', 'askQuestion'])) { + throw $e; + } + } + } + + if (class_exists(Carbon::class)) { + Carbon::setTestNow(); + } + + if (class_exists(CarbonImmutable::class)) { + CarbonImmutable::setTestNow(); + } + + $this->afterApplicationCreatedCallbacks = []; + $this->beforeApplicationDestroyedCallbacks = []; + + if (property_exists($this, 'originalExceptionHandler')) { + $this->originalExceptionHandler = null; + } + + if (property_exists($this, 'originalDeprecationHandler')) { + $this->originalDeprecationHandler = null; + } + + AboutCommand::flushState(); + Artisan::forgetBootstrappers(); + Component::flushCache(); + Component::forgetComponentsResolver(); + Component::forgetFactory(); + ConvertEmptyStringsToNull::flushState(); + EncryptCookies::flushState(); + HandleExceptions::flushState(); + Once::flush(); + PreventRequestsDuringMaintenance::flushState(); + Queue::createPayloadUsing(null); + RegisterProviders::flushState(); + Sleep::fake(false); + TrimStrings::flushState(); + TrustProxies::flushState(); + TrustHosts::flushState(); + ValidateCsrfToken::flushState(); + + if ($this->callbackException) { + throw $this->callbackException; + } + } + + /** + * Boot the testing helper traits. + * + * @return array + */ + protected function setUpTraits() + { + $uses = array_flip(class_uses_recursive(static::class)); + + if (isset($uses[RefreshDatabase::class])) { + $this->refreshDatabase(); + } + + if (isset($uses[DatabaseMigrations::class])) { + $this->runDatabaseMigrations(); + } + + if (isset($uses[DatabaseTruncation::class])) { + $this->truncateDatabaseTables(); + } + + if (isset($uses[DatabaseTransactions::class])) { + $this->beginDatabaseTransaction(); + } + + if (isset($uses[WithoutMiddleware::class])) { + $this->disableMiddlewareForAllTests(); + } + + if (isset($uses[WithFaker::class])) { + $this->setUpFaker(); + } + + foreach ($uses as $trait) { + if (method_exists($this, $method = 'setUp'.class_basename($trait))) { + $this->{$method}(); + } + + if (method_exists($this, $method = 'tearDown'.class_basename($trait))) { + $this->beforeApplicationDestroyed(fn () => $this->{$method}()); + } + } + + return $uses; + } + + /** + * Clean up the testing environment before the next test case. + * + * @internal + * + * @return void + */ + public static function tearDownAfterClassUsingTestCase() + { + (function () { + $this->classDocBlocks = []; + $this->methodDocBlocks = []; + })->call(PHPUnitRegistry::getInstance()); + } + + /** + * Register a callback to be run after the application is created. + * + * @param callable $callback + * @return void + */ + public function afterApplicationCreated(callable $callback) + { + $this->afterApplicationCreatedCallbacks[] = $callback; + + if ($this->setUpHasRun) { + $callback(); + } + } + + /** + * Register a callback to be run before the application is destroyed. + * + * @param callable $callback + * @return void + */ + protected function beforeApplicationDestroyed(callable $callback) + { + $this->beforeApplicationDestroyedCallbacks[] = $callback; + } + + /** + * Execute the application's pre-destruction callbacks. + * + * @return void + */ + protected function callBeforeApplicationDestroyedCallbacks() + { + foreach ($this->beforeApplicationDestroyedCallbacks as $callback) { + try { + $callback(); + } catch (Throwable $e) { + if (! $this->callbackException) { + $this->callbackException = $e; + } + } + } + } +} diff --git a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php index 8aad234668c9..a5468e378830 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php +++ b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php @@ -583,7 +583,7 @@ public function call($method, $uri, $parameters = [], $cookies = [], $files = [] ); $response = $kernel->handle( - $request = Request::createFromBase($symfonyRequest) + $request = $this->createTestRequest($symfonyRequest) ); $kernel->terminate($request, $response); @@ -710,6 +710,17 @@ protected function followRedirects($response) return $response; } + /** + * Create the request instance used for testing from the given Symfony request. + * + * @param \Symfony\Component\HttpFoundation\Request $symfonyRequest + * @return \Illuminate\Http\Request + */ + protected function createTestRequest($symfonyRequest) + { + return Request::createFromBase($symfonyRequest); + } + /** * Create the test response instance from the given response. * diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php index 6eb86d9b47a7..3f43181eb6be 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php @@ -83,7 +83,7 @@ protected function truncateTablesForConnection(ConnectionInterface $connection, $connection->unsetEventDispatcher(); - collect(static::$allTables[$name] ??= $connection->getDoctrineSchemaManager()->listTableNames()) + collect(static::$allTables[$name] ??= $connection->getSchemaBuilder()->getTableListing()) ->when( property_exists($this, 'tablesToTruncate'), fn ($tables) => $tables->intersect($this->tablesToTruncate), diff --git a/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php b/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php index 98204cceab48..3de593d9406b 100644 --- a/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/LazilyRefreshDatabase.php @@ -17,15 +17,24 @@ public function refreshDatabase() { $database = $this->app->make('db'); - $database->beforeExecuting(function () { + $callback = function () { if (RefreshDatabaseState::$lazilyRefreshed) { return; } RefreshDatabaseState::$lazilyRefreshed = true; + $shouldMockOutput = $this->mockConsoleOutput; + + $this->mockConsoleOutput = false; + $this->baseRefreshDatabase(); - }); + + $this->mockConsoleOutput = $shouldMockOutput; + }; + + $database->beforeStartingTransaction($callback); + $database->beforeExecuting($callback); $this->beforeApplicationDestroyed(function () { RefreshDatabaseState::$lazilyRefreshed = false; diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index ab3fe672d4f1..94ec4c71d5be 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -2,21 +2,8 @@ namespace Illuminate\Foundation\Testing; -use Carbon\CarbonImmutable; -use Illuminate\Console\Application as Artisan; -use Illuminate\Database\Eloquent\Model; -use Illuminate\Foundation\Bootstrap\HandleExceptions; -use Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull; -use Illuminate\Foundation\Http\Middleware\TrimStrings; -use Illuminate\Queue\Queue; -use Illuminate\Support\Carbon; -use Illuminate\Support\Facades\Facade; -use Illuminate\Support\Facades\ParallelTesting; -use Illuminate\Support\Sleep; -use Illuminate\Support\Str; -use Illuminate\View\Component; -use Mockery; -use Mockery\Exception\InvalidCountException; +use Illuminate\Contracts\Console\Kernel; +use Illuminate\Foundation\Application; use PHPUnit\Framework\TestCase as BaseTestCase; use Throwable; @@ -31,51 +18,22 @@ abstract class TestCase extends BaseTestCase Concerns\InteractsWithExceptionHandling, Concerns\InteractsWithSession, Concerns\InteractsWithTime, + Concerns\InteractsWithTestCaseLifecycle, Concerns\InteractsWithViews; /** - * The Illuminate application instance. - * - * @var \Illuminate\Foundation\Application - */ - protected $app; - - /** - * The callbacks that should be run after the application is created. - * - * @var array - */ - protected $afterApplicationCreatedCallbacks = []; - - /** - * The callbacks that should be run before the application is destroyed. - * - * @var array - */ - protected $beforeApplicationDestroyedCallbacks = []; - - /** - * The exception thrown while running an application destruction callback. + * Creates the application. * - * @var \Throwable + * @return \Illuminate\Foundation\Application */ - protected $callbackException; + public function createApplication() + { + $app = require Application::inferBasePath().'/bootstrap/app.php'; - /** - * Indicates if we have made it through the base setUp function. - * - * @var bool - */ - protected $setUpHasRun = false; + $app->make(Kernel::class)->bootstrap(); - /** - * Creates the application. - * - * Needs to be implemented by subclasses. - * - * @return \Symfony\Component\HttpKernel\HttpKernelInterface - */ - abstract public function createApplication(); + return $app; + } /** * Setup the test environment. @@ -86,23 +44,7 @@ protected function setUp(): void { static::$latestResponse = null; - Facade::clearResolvedInstances(); - - if (! $this->app) { - $this->refreshApplication(); - - ParallelTesting::callSetUpTestCaseCallbacks($this); - } - - $this->setUpTraits(); - - foreach ($this->afterApplicationCreatedCallbacks as $callback) { - $callback(); - } - - Model::setEventDispatcher($this->app['events']); - - $this->setUpHasRun = true; + $this->setUpTheTestEnvironment(); } /** @@ -115,52 +57,6 @@ protected function refreshApplication() $this->app = $this->createApplication(); } - /** - * Boot the testing helper traits. - * - * @return array - */ - protected function setUpTraits() - { - $uses = array_flip(class_uses_recursive(static::class)); - - if (isset($uses[RefreshDatabase::class])) { - $this->refreshDatabase(); - } - - if (isset($uses[DatabaseMigrations::class])) { - $this->runDatabaseMigrations(); - } - - if (isset($uses[DatabaseTruncation::class])) { - $this->truncateDatabaseTables(); - } - - if (isset($uses[DatabaseTransactions::class])) { - $this->beginDatabaseTransaction(); - } - - if (isset($uses[WithoutMiddleware::class])) { - $this->disableMiddlewareForAllTests(); - } - - if (isset($uses[WithFaker::class])) { - $this->setUpFaker(); - } - - foreach ($uses as $trait) { - if (method_exists($this, $method = 'setUp'.class_basename($trait))) { - $this->{$method}(); - } - - if (method_exists($this, $method = 'tearDown'.class_basename($trait))) { - $this->beforeApplicationDestroyed(fn () => $this->{$method}()); - } - } - - return $uses; - } - /** * {@inheritdoc} */ @@ -184,67 +80,7 @@ protected function transformException(Throwable $error): Throwable */ protected function tearDown(): void { - if ($this->app) { - $this->callBeforeApplicationDestroyedCallbacks(); - - ParallelTesting::callTearDownTestCaseCallbacks($this); - - $this->app->flush(); - - $this->app = null; - } - - $this->setUpHasRun = false; - - if (property_exists($this, 'serverVariables')) { - $this->serverVariables = []; - } - - if (property_exists($this, 'defaultHeaders')) { - $this->defaultHeaders = []; - } - - if (class_exists('Mockery')) { - if ($container = Mockery::getContainer()) { - $this->addToAssertionCount($container->mockery_getExpectationCount()); - } - - try { - Mockery::close(); - } catch (InvalidCountException $e) { - if (! Str::contains($e->getMethodName(), ['doWrite', 'askQuestion'])) { - throw $e; - } - } - } - - if (class_exists(Carbon::class)) { - Carbon::setTestNow(); - } - - if (class_exists(CarbonImmutable::class)) { - CarbonImmutable::setTestNow(); - } - - $this->afterApplicationCreatedCallbacks = []; - $this->beforeApplicationDestroyedCallbacks = []; - - $this->originalExceptionHandler = null; - $this->originalDeprecationHandler = null; - - Artisan::forgetBootstrappers(); - Component::flushCache(); - Component::forgetComponentsResolver(); - Component::forgetFactory(); - ConvertEmptyStringsToNull::flushState(); - HandleExceptions::forgetApp(); - Queue::createPayloadUsing(null); - Sleep::fake(false); - TrimStrings::flushState(); - - if ($this->callbackException) { - throw $this->callbackException; - } + $this->tearDownTheTestEnvironment(); } /** @@ -256,60 +92,6 @@ public static function tearDownAfterClass(): void { static::$latestResponse = null; - foreach ([ - \PHPUnit\Util\Annotation\Registry::class, - \PHPUnit\Metadata\Annotation\Parser\Registry::class, - ] as $class) { - if (class_exists($class)) { - (function () { - $this->classDocBlocks = []; - $this->methodDocBlocks = []; - })->call($class::getInstance()); - } - } - } - - /** - * Register a callback to be run after the application is created. - * - * @param callable $callback - * @return void - */ - public function afterApplicationCreated(callable $callback) - { - $this->afterApplicationCreatedCallbacks[] = $callback; - - if ($this->setUpHasRun) { - $callback(); - } - } - - /** - * Register a callback to be run before the application is destroyed. - * - * @param callable $callback - * @return void - */ - protected function beforeApplicationDestroyed(callable $callback) - { - $this->beforeApplicationDestroyedCallbacks[] = $callback; - } - - /** - * Execute the application's pre-destruction callbacks. - * - * @return void - */ - protected function callBeforeApplicationDestroyedCallbacks() - { - foreach ($this->beforeApplicationDestroyedCallbacks as $callback) { - try { - $callback(); - } catch (Throwable $e) { - if (! $this->callbackException) { - $this->callbackException = $e; - } - } - } + static::tearDownAfterClassUsingTestCase(); } } diff --git a/src/Illuminate/Foundation/Vite.php b/src/Illuminate/Foundation/Vite.php index 9c0a24d04f98..a81ca1616b96 100644 --- a/src/Illuminate/Foundation/Vite.php +++ b/src/Illuminate/Foundation/Vite.php @@ -55,6 +55,13 @@ class Vite implements Htmlable */ protected $manifestFilename = 'manifest.json'; + /** + * The custom asset path resolver. + * + * @var callable|null + */ + protected $assetPathResolver = null; + /** * The script tag attributes resolvers. * @@ -160,6 +167,19 @@ public function useManifestFilename($filename) return $this; } + /** + * Resolve asset paths using the provided resolver. + * + * @param callable|null $urlResolver + * @return $this + */ + public function createAssetPathsUsing($resolver) + { + $this->assetPathResolver = $resolver; + + return $this; + } + /** * Get the Vite "hot" file path. * @@ -688,7 +708,7 @@ public function content($asset, $buildDirectory = null) */ protected function assetPath($path, $secure = null) { - return asset($path, $secure); + return ($this->assetPathResolver ?? asset(...))($path, $secure); } /** diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index e4c15edca725..c698bbdfaa8e 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -929,7 +929,7 @@ function trans($key = null, $replace = [], $locale = null) * Translates the given message based on a count. * * @param string $key - * @param \Countable|int|array $number + * @param \Countable|int|float|array $number * @param array $replace * @param string|null $locale * @return string diff --git a/src/Illuminate/Foundation/resources/health-up.blade.php b/src/Illuminate/Foundation/resources/health-up.blade.php new file mode 100644 index 000000000000..cb4689a89ed2 --- /dev/null +++ b/src/Illuminate/Foundation/resources/health-up.blade.php @@ -0,0 +1,52 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + + + + + + + + + + +
+
+
+
+ + +
+ +
+

Application up

+ +

+ HTTP request received. + + @if (defined('LARAVEL_START')) + Response successfully rendered in {{ round((microtime(true) - LARAVEL_START) * 1000) }}ms. + @endif +

+
+
+
+
+ + diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index d3411ce18777..4bcd13d21b09 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -36,6 +36,13 @@ class Factory */ protected $globalMiddleware = []; + /** + * The options to apply to every request. + * + * @var array + */ + protected $globalOptions = []; + /** * The stub callables that will handle requests. * @@ -123,6 +130,19 @@ public function globalResponseMiddleware($middleware) return $this; } + /** + * Set the options to apply to every request. + * + * @param array $options + * @return $this + */ + public function globalOptions($options) + { + $this->globalOptions = $options; + + return $this; + } + /** * Create a new response instance for use during stubbing. * @@ -400,7 +420,7 @@ public function recorded($callback = null) */ protected function newPendingRequest() { - return new PendingRequest($this, $this->globalMiddleware); + return (new PendingRequest($this, $this->globalMiddleware))->withOptions($this->globalOptions); } /** diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 6966a0f071bf..ea1894557183 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -78,7 +78,7 @@ class PendingRequest /** * The raw body for the request. * - * @var string + * @var \Psr\Http\Message\StreamInterface|string */ protected $pendingBody; @@ -260,7 +260,7 @@ public function baseUrl(string $url) /** * Attach a raw body to the request. * - * @param string $content + * @param \Psr\Http\Message\StreamInterface|string $content * @param string $contentType * @return $this */ @@ -601,13 +601,13 @@ public function connectTimeout(int $seconds) /** * Specify the number of times the request should be attempted. * - * @param int $times + * @param array|int $times * @param Closure|int $sleepMilliseconds * @param callable|null $when * @param bool $throw * @return $this */ - public function retry(int $times, Closure|int $sleepMilliseconds = 0, ?callable $when = null, bool $throw = true) + public function retry(array|int $times, Closure|int $sleepMilliseconds = 0, ?callable $when = null, bool $throw = true) { $this->tries = $times; $this->retryDelay = $sleepMilliseconds; @@ -922,7 +922,7 @@ public function send(string $method, string $url, array $options = []) } }); } catch (ConnectException $e) { - $this->dispatchConnectionFailedEvent(); + $this->dispatchConnectionFailedEvent(new Request($e->getRequest())); throw new ConnectionException($e->getMessage(), 0, $e); } @@ -1012,7 +1012,7 @@ protected function makePromise(string $method, string $url, array $options = [], }) ->otherwise(function (OutOfBoundsException|TransferException $e) { if ($e instanceof ConnectException) { - $this->dispatchConnectionFailedEvent(); + $this->dispatchConnectionFailedEvent(new Request($e->getRequest())); return new ConnectionException($e->getMessage(), 0, $e); } @@ -1465,8 +1465,7 @@ protected function dispatchRequestSendingEvent() */ protected function dispatchResponseReceivedEvent(Response $response) { - if (! ($dispatcher = $this->factory?->getDispatcher()) || - ! $this->request) { + if (! ($dispatcher = $this->factory?->getDispatcher()) || ! $this->request) { return; } @@ -1476,12 +1475,13 @@ protected function dispatchResponseReceivedEvent(Response $response) /** * Dispatch the ConnectionFailed event if a dispatcher is available. * + * @param \Illuminate\Http\Client\Request $request * @return void */ - protected function dispatchConnectionFailedEvent() + protected function dispatchConnectionFailedEvent(Request $request) { if ($dispatcher = $this->factory?->getDispatcher()) { - $dispatcher->dispatch(new ConnectionFailed($this->request)); + $dispatcher->dispatch(new ConnectionFailed($request)); } } diff --git a/src/Illuminate/Http/Middleware/TrustHosts.php b/src/Illuminate/Http/Middleware/TrustHosts.php index c2657f02756e..13dd1f00b62c 100644 --- a/src/Illuminate/Http/Middleware/TrustHosts.php +++ b/src/Illuminate/Http/Middleware/TrustHosts.php @@ -14,6 +14,13 @@ class TrustHosts */ protected $app; + /** + * The trusted hosts that have been configured to always be trusted. + * + * @var array|null + */ + protected static $alwaysTrust; + /** * Create a new middleware instance. * @@ -32,9 +39,9 @@ public function __construct(Application $app) */ public function hosts() { - return [ - $this->allSubdomainsOfApplicationUrl(), - ]; + return is_array(static::$alwaysTrust) + ? static::$alwaysTrust + : [$this->allSubdomainsOfApplicationUrl()]; } /** @@ -53,6 +60,24 @@ public function handle(Request $request, $next) return $next($request); } + /** + * Specify the hosts that should always be trusted. + * + * @param array $hosts + * @param bool $subdomains + * @return void + */ + public static function at(array $hosts, bool $subdomains = true) + { + if ($subdomains) { + if ($host = parse_url(config('app.url'), PHP_URL_HOST)) { + $hosts[] = '^(.+\.)?'.preg_quote($host).'$'; + } + } + + static::$alwaysTrust = $hosts; + } + /** * Determine if the application should specify trusted hosts. * @@ -75,4 +100,14 @@ protected function allSubdomainsOfApplicationUrl() return '^(.+\.)?'.preg_quote($host).'$'; } } + + /** + * Flush the state of the middleware. + * + * @return void + */ + public static function flushState() + { + static::$alwaysTrust = null; + } } diff --git a/src/Illuminate/Http/Middleware/TrustProxies.php b/src/Illuminate/Http/Middleware/TrustProxies.php index 12a2f9b3692e..4b7b0f62b9b1 100644 --- a/src/Illuminate/Http/Middleware/TrustProxies.php +++ b/src/Illuminate/Http/Middleware/TrustProxies.php @@ -12,10 +12,10 @@ class TrustProxies * * @var array|string|null */ - protected $proxies = '*'; + protected $proxies; /** - * The proxy header mappings. + * The trusted proxies headers for the application. * * @var int */ @@ -25,6 +25,20 @@ class TrustProxies Request::HEADER_X_FORWARDED_PROTO | Request::HEADER_X_FORWARDED_AWS_ELB; + /** + * The proxies that have been configured to always be trusted. + * + * @var array|string|null + */ + protected static $alwaysTrustProxies; + + /** + * The proxies headers that have been configured to always be trusted. + * + * @var int|null + */ + protected static $alwaysTrustHeaders; + /** * Handle an incoming request. * @@ -96,11 +110,13 @@ protected function setTrustedProxyIpAddressesToTheCallingIp(Request $request) */ protected function getTrustedHeaderNames() { - if (is_int($this->headers)) { - return $this->headers; + $headers = $this->headers(); + + if (is_int($headers)) { + return $headers; } - return match ($this->headers) { + return match ($headers) { 'HEADER_X_FORWARDED_AWS_ELB' => Request::HEADER_X_FORWARDED_AWS_ELB, 'HEADER_FORWARDED' => Request::HEADER_FORWARDED, 'HEADER_X_FORWARDED_FOR' => Request::HEADER_X_FORWARDED_FOR, @@ -112,6 +128,16 @@ protected function getTrustedHeaderNames() }; } + /** + * Get the trusted headers. + * + * @return int + */ + protected function headers() + { + return static::$alwaysTrustHeaders ?: $this->headers; + } + /** * Get the trusted proxies. * @@ -119,6 +145,39 @@ protected function getTrustedHeaderNames() */ protected function proxies() { - return $this->proxies; + return static::$alwaysTrustProxies ?: $this->proxies; + } + + /** + * Specify the IP addresses of proxies that should always be trusted. + * + * @param array|string $proxies + * @return void + */ + public static function at(array|string $proxies) + { + static::$alwaysTrustProxies = $proxies; + } + + /** + * Specify the proxy headers that should always be trusted. + * + * @param int $headers + * @return void + */ + public static function withHeaders(int $headers) + { + static::$alwaysTrustHeaders = $headers; + } + + /** + * Flush the state of the middleware. + * + * @return void + */ + public static function flushState() + { + static::$alwaysTrustHeaders = null; + static::$alwaysTrustProxies = null; } } diff --git a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php index 3f25ca052407..252fa8ba4e59 100644 --- a/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php +++ b/src/Illuminate/Http/Resources/ConditionallyLoadsAttributes.php @@ -266,15 +266,17 @@ protected function whenLoaded($relationship, $value = null, $default = null) return value($default); } + $loadedValue = $this->resource->{$relationship}; + if (func_num_args() === 1) { - return $this->resource->{$relationship}; + return $loadedValue; } - if ($this->resource->{$relationship} === null) { + if ($loadedValue === null) { return; } - return value($value); + return value($value, $loadedValue); } /** @@ -293,7 +295,7 @@ public function whenCounted($relationship, $value = null, $default = null) $attribute = (string) Str::of($relationship)->snake()->finish('_count'); - if (! isset($this->resource->getAttributes()[$attribute])) { + if (! array_key_exists($attribute, $this->resource->getAttributes())) { return value($default); } @@ -320,9 +322,13 @@ public function whenCounted($relationship, $value = null, $default = null) */ public function whenAggregated($relationship, $column, $aggregate, $value = null, $default = null) { + if (func_num_args() < 5) { + $default = new MissingValue; + } + $attribute = (string) Str::of($relationship)->snake()->append('_')->append($aggregate)->append('_')->finish($column); - if (! isset($this->resource->getAttributes()[$attribute])) { + if (! array_key_exists($attribute, $this->resource->getAttributes())) { return value($default); } diff --git a/src/Illuminate/Http/composer.json b/src/Illuminate/Http/composer.json index f683ad8e8390..9222aedd597b 100755 --- a/src/Illuminate/Http/composer.json +++ b/src/Illuminate/Http/composer.json @@ -17,6 +17,7 @@ "php": "^8.2", "ext-filter": "*", "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", "illuminate/collections": "^11.0", "illuminate/macroable": "^11.0", @@ -33,8 +34,7 @@ } }, "suggest": { - "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", - "guzzlehttp/guzzle": "Required to use the HTTP Client (^7.6)." + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image()." }, "extra": { "branch-alias": { diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index 60498f4ef06d..b3ece35d811c 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -501,6 +501,22 @@ public function sharedContext() return $this->sharedContext; } + /** + * Flush the log context on all currently resolved channels. + * + * @return $this + */ + public function withoutContext() + { + foreach ($this->channels as $channel) { + if (method_exists($channel, 'withoutContext')) { + $channel->withoutContext(); + } + } + + return $this; + } + /** * Flush the shared context. * @@ -614,7 +630,7 @@ public function getChannels() /** * System is unusable. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -629,7 +645,7 @@ public function emergency($message, array $context = []): void * Example: Entire website down, database unavailable, etc. This should * trigger the SMS alerts and wake you up. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -643,7 +659,7 @@ public function alert($message, array $context = []): void * * Example: Application component unavailable, unexpected exception. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -656,7 +672,7 @@ public function critical($message, array $context = []): void * Runtime errors that do not require immediate action but should typically * be logged and monitored. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -671,7 +687,7 @@ public function error($message, array $context = []): void * Example: Use of deprecated APIs, poor use of an API, undesirable things * that are not necessarily wrong. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -683,7 +699,7 @@ public function warning($message, array $context = []): void /** * Normal but significant events. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -697,7 +713,7 @@ public function notice($message, array $context = []): void * * Example: User logs in, SQL logs. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -709,7 +725,7 @@ public function info($message, array $context = []): void /** * Detailed debug information. * - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ @@ -722,7 +738,7 @@ public function debug($message, array $context = []): void * Logs with an arbitrary level. * * @param mixed $level - * @param string $message + * @param string|\Stringable $message * @param array $context * @return void */ diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index ecf094af2c15..80cab9856388 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -9,6 +9,7 @@ use Illuminate\Log\LogManager; use Illuminate\Mail\Transport\ArrayTransport; use Illuminate\Mail\Transport\LogTransport; +use Illuminate\Mail\Transport\ResendTransport; use Illuminate\Mail\Transport\SesTransport; use Illuminate\Mail\Transport\SesV2Transport; use Illuminate\Support\Arr; @@ -16,11 +17,13 @@ use Illuminate\Support\Str; use InvalidArgumentException; use Psr\Log\LoggerInterface; +use Resend; use Symfony\Component\HttpClient\HttpClient; use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; use Symfony\Component\Mailer\Transport\Dsn; use Symfony\Component\Mailer\Transport\FailoverTransport; +use Symfony\Component\Mailer\Transport\RoundRobinTransport; use Symfony\Component\Mailer\Transport\SendmailTransport; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory; @@ -291,6 +294,19 @@ protected function addSesCredentials(array $config) return Arr::except($config, ['token']); } + /** + * Create an instance of the Resend Transport driver. + * + * @param array $config + * @return \Illuminate\Mail\Transport\ResendTransprot + */ + protected function createResendTransport(array $config) + { + return new ResendTransport( + Resend::client($this->app['config']->get('services.resend.key')), + ); + } + /** * Create an instance of the Symfony Mail Transport driver. * @@ -375,6 +391,34 @@ protected function createFailoverTransport(array $config) return new FailoverTransport($transports); } + /** + * Create an instance of the Symfony Roundrobin Transport driver. + * + * @param array $config + * @return \Symfony\Component\Mailer\Transport\RoundRobinTransport + */ + protected function createRoundrobinTransport(array $config) + { + $transports = []; + + foreach ($config['mailers'] as $name) { + $config = $this->getConfig($name); + + if (is_null($config)) { + throw new InvalidArgumentException("Mailer [{$name}] is not defined."); + } + + // Now, we will check if the "driver" key exists and if it does we will set + // the transport configuration parameter in order to offer compatibility + // with any Laravel <= 6.x application style mail configuration files. + $transports[] = $this->app['config']['mail.driver'] + ? $this->createSymfonyTransport(array_merge($config, ['transport' => $name])) + : $this->createSymfonyTransport($config); + } + + return new RoundRobinTransport($transports); + } + /** * Create an instance of the Log Transport driver. * diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index 7bdf38ad8541..48831c0e551a 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -347,7 +347,7 @@ public function buildViewData() } foreach ((new ReflectionClass($this))->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { - if ($property->getDeclaringClass()->getName() !== self::class) { + if ($property->isInitialized($this) && $property->getDeclaringClass()->getName() !== self::class) { $data[$property->getName()] = $property->getValue($this); } } @@ -1231,7 +1231,7 @@ public function assertTo($address, $name = null) PHPUnit::assertTrue( $this->hasTo($address, $name), - "Did not see expected recipient [{$recipient}] in email recipients." + "Did not see expected recipient [{$recipient}] in email 'to' recipients." ); return $this; @@ -1264,7 +1264,7 @@ public function assertHasCc($address, $name = null) PHPUnit::assertTrue( $this->hasCc($address, $name), - "Did not see expected recipient [{$recipient}] in email recipients." + "Did not see expected recipient [{$recipient}] in email 'cc' recipients." ); return $this; @@ -1285,7 +1285,7 @@ public function assertHasBcc($address, $name = null) PHPUnit::assertTrue( $this->hasBcc($address, $name), - "Did not see expected recipient [{$recipient}] in email recipients." + "Did not see expected recipient [{$recipient}] in email 'bcc' recipients." ); return $this; diff --git a/src/Illuminate/Mail/Mailer.php b/src/Illuminate/Mail/Mailer.php index 132d95683754..bb8a82069132 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -357,6 +357,21 @@ protected function sendMailable(MailableContract $mailable) : $mailable->mailer($this->name)->send($this); } + /** + * Send a new message synchronously using a view. + * + * @param \Illuminate\Contracts\Mail\Mailable|string|array $view + * @param array $data + * @param \Closure|string|null $callback + * @return \Illuminate\Mail\SentMessage|null + */ + public function sendNow($mailable, array $data = [], $callback = null) + { + return $mailable instanceof MailableContract + ? $mailable->mailer($this->name)->send($this) + : $this->send($mailable, $data, $callback); + } + /** * Parse the given view name or array. * @@ -450,7 +465,7 @@ protected function setGlobalToAndRemoveCcAndBcc($message) } /** - * Queue a new e-mail message for sending. + * Queue a new mail message for sending. * * @param \Illuminate\Contracts\Mail\Mailable|string|array $view * @param string|null $queue @@ -472,7 +487,7 @@ public function queue($view, $queue = null) } /** - * Queue a new e-mail message for sending on the given queue. + * Queue a new mail message for sending on the given queue. * * @param string $queue * @param \Illuminate\Contracts\Mail\Mailable $view @@ -484,7 +499,7 @@ public function onQueue($queue, $view) } /** - * Queue a new e-mail message for sending on the given queue. + * Queue a new mail message for sending on the given queue. * * This method didn't match rest of framework's "onQueue" phrasing. Added "onQueue". * @@ -498,7 +513,7 @@ public function queueOn($queue, $view) } /** - * Queue a new e-mail message for sending after (n) seconds. + * Queue a new mail message for sending after (n) seconds. * * @param \DateTimeInterface|\DateInterval|int $delay * @param \Illuminate\Contracts\Mail\Mailable $view @@ -519,7 +534,7 @@ public function later($delay, $view, $queue = null) } /** - * Queue a new e-mail message for sending after (n) seconds on the given queue. + * Queue a new mail message for sending after (n) seconds on the given queue. * * @param string $queue * @param \DateTimeInterface|\DateInterval|int $delay diff --git a/src/Illuminate/Mail/PendingMail.php b/src/Illuminate/Mail/PendingMail.php index 330b6438bada..1aa3d9a6cc2f 100644 --- a/src/Illuminate/Mail/PendingMail.php +++ b/src/Illuminate/Mail/PendingMail.php @@ -124,6 +124,17 @@ public function send(MailableContract $mailable) return $this->mailer->send($this->fill($mailable)); } + /** + * Send a new mailable message instance synchronously. + * + * @param \Illuminate\Contracts\Mail\Mailable $mailable + * @return \Illuminate\Mail\SentMessage|null + */ + public function sendNow(MailableContract $mailable) + { + return $this->mailer->sendNow($this->fill($mailable)); + } + /** * Push the given mailable onto the queue. * diff --git a/src/Illuminate/Mail/Transport/LogTransport.php b/src/Illuminate/Mail/Transport/LogTransport.php index 3d05a9956558..848733700586 100644 --- a/src/Illuminate/Mail/Transport/LogTransport.php +++ b/src/Illuminate/Mail/Transport/LogTransport.php @@ -2,6 +2,7 @@ namespace Illuminate\Mail\Transport; +use Illuminate\Support\Str; use Psr\Log\LoggerInterface; use Stringable; use Symfony\Component\Mailer\Envelope; @@ -34,17 +35,48 @@ public function __construct(LoggerInterface $logger) */ public function send(RawMessage $message, Envelope $envelope = null): ?SentMessage { - $string = $message->toString(); + $string = Str::of($message->toString()); - if (str_contains($string, 'Content-Transfer-Encoding: quoted-printable')) { - $string = quoted_printable_decode($string); + if ($string->contains('Content-Type: multipart/')) { + $boundary = $string + ->after('boundary=') + ->before("\r\n") + ->prepend('--') + ->append("\r\n"); + + $string = $string + ->explode($boundary) + ->map($this->decodeQuotedPrintableContent(...)) + ->implode($boundary); + } elseif ($string->contains('Content-Transfer-Encoding: quoted-printable')) { + $string = $this->decodeQuotedPrintableContent($string); } - $this->logger->debug($string); + $this->logger->debug((string) $string); return new SentMessage($message, $envelope ?? Envelope::create($message)); } + /** + * Decode the given quoted printable content. + * + * @param string $part + * @return string + */ + protected function decodeQuotedPrintableContent(string $part) + { + if (! str_contains($part, 'Content-Transfer-Encoding: quoted-printable')) { + return $part; + } + + [$headers, $content] = explode("\r\n\r\n", $part, 2); + + return implode("\r\n\r\n", [ + $headers, + quoted_printable_decode($content), + ]); + } + /** * Get the logger for the LogTransport instance. * diff --git a/src/Illuminate/Mail/Transport/ResendTransport.php b/src/Illuminate/Mail/Transport/ResendTransport.php new file mode 100644 index 000000000000..a62f4f0f4926 --- /dev/null +++ b/src/Illuminate/Mail/Transport/ResendTransport.php @@ -0,0 +1,129 @@ +getOriginalMessage()); + + $envelope = $message->getEnvelope(); + + $headers = []; + + $headersToBypass = ['from', 'to', 'cc', 'bcc', 'reply-to', 'sender', 'subject', 'content-type']; + + foreach ($email->getHeaders()->all() as $name => $header) { + if (in_array($name, $headersToBypass, true)) { + continue; + } + + $headers[$header->getName()] = $header->getBodyAsString(); + } + + $attachments = []; + + if ($email->getAttachments()) { + foreach ($email->getAttachments() as $attachment) { + $headers = $attachment->getPreparedHeaders(); + + $filename = $headers->getHeaderParameter('Content-Disposition', 'filename'); + + $item = [ + 'content' => str_replace("\r\n", '', $attachment->bodyToString()), + 'filename' => $filename, + ]; + + $attachments[] = $item; + } + } + + try { + $result = $this->resend->emails->send([ + 'from' => $envelope->getSender()->toString(), + 'to' => $this->stringifyAddresses($this->getRecipients($email, $envelope)), + 'cc' => $this->stringifyAddresses($email->getCc()), + 'bcc' => $this->stringifyAddresses($email->getBcc()), + 'reply_to' => $this->stringifyAddresses($email->getReplyTo()), + 'headers' => $headers, + 'subject' => $email->getSubject(), + 'html' => $email->getHtmlBody(), + 'text' => $email->getTextBody(), + 'attachments' => $attachments, + ]); + } catch (Exception $exception) { + throw new TransportException( + sprintf('Request to Resend API failed. Reason: %s.', $exception->getMessage()), + is_int($exception->getCode()) ? $exception->getCode() : 0, + $exception + ); + } + + $messageId = $result->id; + + $email->getHeaders()->addHeader('X-Resend-Email-ID', $messageId); + } + + /** + * Get the recipients without CC or BCC. + */ + protected function getRecipients(Email $email, Envelope $envelope): array + { + return array_filter($envelope->getRecipients(), function (Address $address) use ($email) { + return in_array($address, array_merge($email->getCc(), $email->getBcc()), true) === false; + }); + } + + /** + * Get the string representation of the transport. + */ + public function __toString(): string + { + return 'resend'; + } +} diff --git a/src/Illuminate/Mail/composer.json b/src/Illuminate/Mail/composer.json index ad0211e96d7c..cf00d7cc483c 100755 --- a/src/Illuminate/Mail/composer.json +++ b/src/Illuminate/Mail/composer.json @@ -37,6 +37,7 @@ }, "suggest": { "aws/aws-sdk-php": "Required to use the SES mail driver (^3.235.5).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", "symfony/http-client": "Required to use the Symfony API mail transports (^7.0).", "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0)." diff --git a/src/Illuminate/Mail/resources/views/html/message.blade.php b/src/Illuminate/Mail/resources/views/html/message.blade.php index f272460daa32..1a874fc26de5 100644 --- a/src/Illuminate/Mail/resources/views/html/message.blade.php +++ b/src/Illuminate/Mail/resources/views/html/message.blade.php @@ -21,7 +21,7 @@ {{-- Footer --}} -© {{ date('Y') }} {{ config('app.name') }}. @lang('All rights reserved.') +© {{ date('Y') }} {{ config('app.name') }}. {{ __('All rights reserved.') }} diff --git a/src/Illuminate/Notifications/Messages/MailMessage.php b/src/Illuminate/Notifications/Messages/MailMessage.php index d847072c8f59..280f2e1f2e2e 100644 --- a/src/Illuminate/Notifications/Messages/MailMessage.php +++ b/src/Illuminate/Notifications/Messages/MailMessage.php @@ -129,6 +129,21 @@ public function view($view, array $data = []) return $this; } + /** + * Set the plain text view for the mail message. + * + * @param string $textView + * @param array $data + * @return $this + */ + public function text($textView, array $data = []) + { + return $this->view([ + 'html' => is_array($this->view) ? ($this->view['html'] ?? null) : $this->view, + 'text' => $textView, + ], $data); + } + /** * Set the Markdown template for the notification. * diff --git a/src/Illuminate/Pagination/resources/views/simple-tailwind.blade.php b/src/Illuminate/Pagination/resources/views/simple-tailwind.blade.php index 6872cca360d5..ea02400f468c 100644 --- a/src/Illuminate/Pagination/resources/views/simple-tailwind.blade.php +++ b/src/Illuminate/Pagination/resources/views/simple-tailwind.blade.php @@ -2,22 +2,22 @@