diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e48024c..fb326f9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: if: "!contains(github.event.head_commit.message, '[ci skip]')" strategy: matrix: - php: ['8.1', '8.2'] + php: ['8.2'] steps: - name: Checkout the project diff --git a/bin/acorn b/bin/acorn index 29563b64..4d5a3562 100755 --- a/bin/acorn +++ b/bin/acorn @@ -2,7 +2,7 @@ boot(); + Roots\Acorn\Application::configure()->boot(); })(); diff --git a/composer.json b/composer.json index 84c51b74..726a8af2 100644 --- a/composer.json +++ b/composer.json @@ -42,46 +42,46 @@ } }, "require": { - "php": ">=8.1", + "php": ">=8.2", "ext-json": "*", "ext-mbstring": "*", "guzzlehttp/guzzle": "^7.8", - "illuminate/cache": "^10.43", - "illuminate/config": "^10.43", - "illuminate/console": "^10.43", - "illuminate/container": "^10.43", - "illuminate/contracts": "^10.43", - "illuminate/database": "^10.43", - "illuminate/encryption": "^10.43", - "illuminate/events": "^10.43", - "illuminate/filesystem": "^10.43", - "illuminate/http": "^10.43", - "illuminate/log": "^10.43", - "illuminate/queue": "^10.43", - "illuminate/routing": "^10.43", - "illuminate/support": "^10.43", - "illuminate/validation": "^10.43", - "illuminate/view": "^10.43", - "laravel/prompts": "^0.1.7", + "illuminate/cache": "^11.0", + "illuminate/config": "^11.0", + "illuminate/console": "^11.0", + "illuminate/container": "^11.0", + "illuminate/contracts": "^11.0", + "illuminate/database": "^11.0", + "illuminate/encryption": "^11.0", + "illuminate/events": "^11.0", + "illuminate/filesystem": "^11.0", + "illuminate/http": "^11.0", + "illuminate/log": "^11.0", + "illuminate/queue": "^11.0", + "illuminate/routing": "^11.0", + "illuminate/support": "^11.0", + "illuminate/validation": "^11.0", + "illuminate/view": "^11.0", + "laravel/prompts": "^0.1.17", "laravel/serializable-closure": "^1.3", - "league/flysystem": "^3.8", + "league/flysystem": "^3.26", "ramsey/uuid": "^4.7", "roots/support": "^1.0", - "symfony/error-handler": "^6.2", - "symfony/var-dumper": "^6.2", - "vlucas/phpdotenv": "^5.4.1" + "symfony/error-handler": "^7.0", + "symfony/var-dumper": "^7.0", + "vlucas/phpdotenv": "^5.6" }, "require-dev": { - "laravel/pint": "1.14", + "laravel/pint": "^1.15", "mockery/mockery": "^1.6", - "pestphp/pest": "^2.25", + "pestphp/pest": "^2.34", "phpcompatibility/php-compatibility": "^9.3", "roave/security-advisories": "dev-master", - "spatie/laravel-ignition": "^2.1", + "spatie/laravel-ignition": "^2.5", "spatie/pest-plugin-snapshots": "^2.1", - "spatie/temporary-directory": "^2.0", + "spatie/temporary-directory": "^2.2", "tmarsteel/mockery-callable-mock": "^2.1", - "wp-cli/wp-cli": "^2.5" + "wp-cli/wp-cli": "^2.10" }, "suggest": { "roots/acorn-prettify": "A collection of modules to apply theme-agnostic front-end modifications (^1.0).", diff --git a/config-stubs/app.php b/config-stubs/app.php new file mode 100755 index 00000000..e71a9cc8 --- /dev/null +++ b/config-stubs/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Acorn'), + + /* + |-------------------------------------------------------------------------- + | 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' => defined('WP_ENV') ? WP_ENV : env('WP_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' => WP_DEBUG && WP_DEBUG_DISPLAY, + + /* + |-------------------------------------------------------------------------- + | 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', home_url()), + + /* + |-------------------------------------------------------------------------- + | 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' => get_option('timezone_string') ?: 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', get_locale()), + + '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 old mode 100644 new mode 100755 index 59cd3d36..0f4ec6d1 --- a/config/app.php +++ b/config/app.php @@ -12,9 +12,9 @@ | Application Name |-------------------------------------------------------------------------- | - | This value is the name of your application. This value is used when the + | 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. | */ @@ -53,12 +53,14 @@ | | 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. | */ 'url' => env('APP_URL', home_url()), + 'frontend_url' => env('FRONTEND_URL', 'http://localhost:3000'), + 'asset_url' => env('ASSET_URL'), /* @@ -67,12 +69,12 @@ |-------------------------------------------------------------------------- | | Here you may specify the default timezone for your application, which - | will be used by the PHP date and date-time functions. We have gone - | ahead and set this to a sensible default for you out of the box. + | 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' => get_option('timezone_string') ?: 'UTC', + 'timezone' => get_option('timezone_string') ?: env('APP_TIMEZONE', 'UTC'), /* |-------------------------------------------------------------------------- @@ -80,25 +82,25 @@ |-------------------------------------------------------------------------- | | 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. | */ - 'locale' => get_locale(), + 'locale' => env('APP_LOCALE', get_locale()), /* |-------------------------------------------------------------------------- | Application Fallback Locale |-------------------------------------------------------------------------- | - | The fallback locale determines the locale to use when the current one + | The fallback locale determines the locale to use when the default one | is not available. You may change the value to correspond to any of - | the language folders that are provided through your application. + | the languages which are currently supported by your application. | */ - 'fallback_locale' => 'en', + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), /* |-------------------------------------------------------------------------- @@ -111,22 +113,28 @@ | */ - 'faker_locale' => 'en_US', + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), /* |-------------------------------------------------------------------------- | Encryption Key |-------------------------------------------------------------------------- | - | This key is used by the Illuminate encrypter service and should be set - | to a random, 32 character string, otherwise these encrypted strings - | will not be safe. Please do this before deploying an application! + | 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'), - 'cipher' => 'AES-256-CBC', + 'previous_keys' => [ + ...array_filter( + explode(',', env('APP_PREVIOUS_KEYS', '')) + ), + ], /* |-------------------------------------------------------------------------- @@ -142,8 +150,8 @@ */ 'maintenance' => [ - 'driver' => 'file', - // 'store' => 'redis', + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), ], /* @@ -151,21 +159,19 @@ | Autoloaded Service Providers |-------------------------------------------------------------------------- | - | The service providers listed here will be automatically loaded on the - | request to your application. Feel free to add your own services to - | this array to grant expanded functionality to your applications. + | The service providers listed here will be automatically loaded on any + | requests to your application. You may add your own services to the + | arrays below to provide additional features to this application. | */ 'providers' => ServiceProvider::defaultProviders()->merge([ - /* - * Package Service Providers... - */ - - /* - * Application Service Providers... - */ - // App\Providers\ThemeServiceProvider::class, + // Package Service Providers... + ])->merge([ + // Application Service Providers... + // App\Providers\AppServiceProvider::class, + ])->merge([ + // Added Service Providers (Do not remove this line)... ])->toArray(), /* @@ -174,8 +180,8 @@ |-------------------------------------------------------------------------- | | This array of class aliases will be registered when this application - | is started. However, feel free to register as many as you wish as - | the aliases are "lazy" loaded so they don't hinder performance. + | is started. You may add any additional class aliases which should + | be loaded to the array. For speed, all aliases are lazy loaded. | */ diff --git a/config/auth.php b/config/auth.php old mode 100644 new mode 100755 index 9548c15d..0ba5d5d8 --- 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' => 'web', - 'passwords' => 'users', + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), ], /* @@ -25,11 +25,11 @@ | | Next, you may define every authentication guard for your application. | Of course, a great default configuration has been defined for you - | here which uses session storage and the Eloquent user provider. + | 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" @@ -62,7 +62,7 @@ 'providers' => [ 'users' => [ 'driver' => 'eloquent', - 'model' => App\Models\User::class, + 'model' => env('AUTH_MODEL', App\Models\User::class), ], // 'users' => [ @@ -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, ], @@ -105,11 +105,11 @@ |-------------------------------------------------------------------------- | | Here you may define the amount of seconds before a password confirmation - | times out and the user is prompted to re-enter their password via the + | window expires and users are asked to re-enter their password via the | confirmation screen. By default, the timeout lasts for three hours. | */ - 'password_timeout' => 10800, + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), ]; diff --git a/config/broadcasting.php b/config/broadcasting.php old mode 100644 new mode 100755 index 24104853..ebc3fb9c --- a/config/broadcasting.php +++ b/config/broadcasting.php @@ -11,11 +11,11 @@ | 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" | */ - 'default' => env('BROADCAST_DRIVER', 'null'), + 'default' => env('BROADCAST_CONNECTION', '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' => 'default', - ], - 'log' => [ 'driver' => 'log', ], diff --git a/config/cache.php b/config/cache.php old mode 100644 new mode 100755 index d4171e22..8aa98219 --- a/config/cache.php +++ b/config/cache.php @@ -9,13 +9,13 @@ | Default Cache Store |-------------------------------------------------------------------------- | - | This option controls the default cache connection that gets used while - | using this caching library. This connection is used when another is - | not explicitly specified when executing a given caching function. + | This option controls the default cache store that will be used by the + | framework. This connection is utilized if another isn't explicitly + | specified when running a cache operation inside the application. | */ - 'default' => env('CACHE_DRIVER', 'file'), + 'default' => env('CACHE_STORE', 'file'), /* |-------------------------------------------------------------------------- @@ -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, @@ -44,9 +40,9 @@ 'database' => [ 'driver' => 'database', - 'table' => 'cache', - 'connection' => null, - 'lock_connection' => null, + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'connection' => env('DB_CACHE_CONNECTION'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), ], 'file' => [ @@ -76,8 +72,8 @@ 'redis' => [ 'driver' => 'redis', - 'connection' => 'cache', - 'lock_connection' => 'default', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), ], 'dynamodb' => [ @@ -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/cors.php b/config/cors.php old mode 100644 new mode 100755 diff --git a/config/database.php b/config/database.php old mode 100644 new mode 100755 index 7f809573..aac5c2f5 --- a/config/database.php +++ b/config/database.php @@ -10,8 +10,9 @@ |-------------------------------------------------------------------------- | | 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 using the Database library. + | 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. | */ @@ -22,14 +23,9 @@ | 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 the framework is shown below to make development simple. - | - | - | All database work in the framework is done through the PHP PDO facilities - | so make sure you have the driver for your particular database of - | choice installed on your machine before you begin development. + | 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. | */ @@ -37,7 +33,7 @@ 'sqlite' => [ 'driver' => 'sqlite', - 'url' => env('DATABASE_URL'), + 'url' => env('DB_URL'), 'database' => env('DB_DATABASE', database_path('database.sqlite')), 'prefix' => '', 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), @@ -63,15 +59,35 @@ 'mysql' => [ 'driver' => 'mysql', - 'url' => env('DATABASE_URL'), + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + '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', - 'collation' => 'utf8mb4_unicode_ci', + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, @@ -83,13 +99,13 @@ 'pgsql' => [ 'driver' => 'pgsql', - 'url' => env('DATABASE_URL'), + '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', + 'charset' => env('DB_CHARSET', 'utf8'), 'prefix' => '', 'prefix_indexes' => true, 'search_path' => 'public', @@ -98,13 +114,13 @@ 'sqlsrv' => [ 'driver' => 'sqlsrv', - 'url' => env('DATABASE_URL'), + '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', + 'charset' => env('DB_CHARSET', 'utf8'), 'prefix' => '', 'prefix_indexes' => true, // 'encrypt' => env('DB_ENCRYPT', 'yes'), @@ -120,11 +136,14 @@ | | 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. | */ - 'migrations' => 'migrations', + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], /* |-------------------------------------------------------------------------- @@ -133,7 +152,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. The framework makes it easy to dig right in. + | such as Memcached. You may define your connection settings here. | */ @@ -143,7 +162,7 @@ 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), - 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'application'), '_').'_database_'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), ], 'default' => [ diff --git a/config/filesystems.php b/config/filesystems.php old mode 100644 new mode 100755 index 20e29f0b..084a506b --- a/config/filesystems.php +++ b/config/filesystems.php @@ -9,7 +9,7 @@ | | Here you may specify the default filesystem disk that should be used | by the framework. The "local" disk, as well as a variety of cloud - | based disks are available to your application. Just store away! + | based disks are available to your application for file storage. | */ @@ -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/hashing.php b/config/hashing.php old mode 100644 new mode 100755 index 0e8a0bb3..9eb408e0 --- a/config/hashing.php +++ b/config/hashing.php @@ -15,7 +15,7 @@ | */ - 'driver' => 'bcrypt', + 'driver' => env('HASH_DRIVER', 'bcrypt'), /* |-------------------------------------------------------------------------- @@ -30,7 +30,7 @@ 'bcrypt' => [ 'rounds' => env('BCRYPT_ROUNDS', 12), - 'verify' => true, + 'verify' => env('HASH_VERIFY', true), ], /* @@ -45,10 +45,23 @@ */ 'argon' => [ - 'memory' => 65536, - 'threads' => 1, - 'time' => 4, - 'verify' => true, + 'memory' => env('ARGON_MEMORY', 65536), + 'threads' => env('ARGON_THREADS', 1), + 'time' => env('ARGON_TIME', 4), + 'verify' => env('HASH_VERIFY', true), ], + /* + |-------------------------------------------------------------------------- + | Rehash On Login + |-------------------------------------------------------------------------- + | + | Setting this option to true will tell Laravel to automatically rehash + | the user's password during login if the configured work factor for + | the algorithm has changed, allowing graceful upgrades of hashes. + | + */ + + 'rehash_on_login' => true, + ]; diff --git a/config/logging.php b/config/logging.php old mode 100644 new mode 100755 index 54e27ed2..d526b64d --- a/config/logging.php +++ b/config/logging.php @@ -12,13 +12,13 @@ | 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. | */ - 'default' => env('LOG_CHANNEL', defined('WP_ENV') && WP_ENV === 'development' ? 'stack' : 'null'), + 'default' => env('LOG_CHANNEL', 'stack'), /* |-------------------------------------------------------------------------- @@ -33,7 +33,7 @@ 'deprecations' => [ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), - 'trace' => false, + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), ], /* @@ -41,43 +41,43 @@ | Log Channels |-------------------------------------------------------------------------- | - | Here you may configure the log channels for your application. Out of - | the box, the framework 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' => ['single'], + 'channels' => explode(',', env('LOG_STACK', 'single')), 'ignore_exceptions' => false, ], 'single' => [ 'driver' => 'single', - 'path' => storage_path('logs/application.log'), + 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'replace_placeholders' => true, ], 'daily' => [ 'driver' => 'daily', - 'path' => storage_path('logs/application.log'), + 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), - 'days' => 14, + 'days' => env('LOG_DAILY_DAYS', 14), 'replace_placeholders' => true, ], 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), - 'username' => 'Application Log', - 'emoji' => ':boom:', + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), 'level' => env('LOG_LEVEL', 'critical'), 'replace_placeholders' => true, ], @@ -108,7 +108,7 @@ 'syslog' => [ 'driver' => 'syslog', 'level' => env('LOG_LEVEL', 'debug'), - 'facility' => LOG_USER, + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), 'replace_placeholders' => true, ], @@ -124,8 +124,9 @@ ], 'emergency' => [ - 'path' => storage_path('logs/application.log'), + 'path' => storage_path('logs/laravel.log'), ], + ], ]; diff --git a/config/mail.php b/config/mail.php old mode 100644 new mode 100755 index a0962d2e..47e3eed4 --- a/config/mail.php +++ b/config/mail.php @@ -7,9 +7,10 @@ | 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. | */ @@ -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. | - | The framework supports a variety of mail "transport" drivers to be used - | while sending an e-mail. You will specify which one you are 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', ], ], + ], /* @@ -93,9 +89,9 @@ | Global "From" Address |-------------------------------------------------------------------------- | - | You may wish for all e-mails sent by your application to be sent from - | the same address. Here, you may specify a name and address that is - | used globally for all e-mails that are sent by your application. + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. | */ @@ -111,12 +107,12 @@ | | If you are using Markdown based email rendering, you may configure your | theme and component paths here, allowing you to customize the design - | of the emails. Or, you may simply stick with the framework defaults! + | of the emails. Or, you may simply stick with the Laravel defaults! | */ 'markdown' => [ - 'theme' => 'default', + 'theme' => env('MAIL_MARKDOWN_THEME', 'default'), 'paths' => [ resource_path('views/vendor/mail'), diff --git a/config/queue.php b/config/queue.php old mode 100644 new mode 100755 index 58f68d48..85280291 --- a/config/queue.php +++ b/config/queue.php @@ -7,9 +7,9 @@ | Default Queue Connection Name |-------------------------------------------------------------------------- | - | The framework'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 the framework. 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,17 +36,18 @@ 'database' => [ 'driver' => 'database', - 'table' => 'jobs', - 'queue' => 'default', - 'retry_after' => 90, + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 'after_commit' => false, ], 'beanstalkd' => [ 'driver' => 'beanstalkd', - 'host' => 'localhost', - 'queue' => 'default', - 'retry_after' => 90, + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), 'block_for' => 0, 'after_commit' => false, ], @@ -64,9 +65,9 @@ 'redis' => [ 'driver' => 'redis', - 'connection' => 'default', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 'queue' => env('REDIS_QUEUE', 'default'), - 'retry_after' => 90, + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 'block_for' => null, 'after_commit' => false, ], @@ -85,7 +86,7 @@ */ 'batching' => [ - 'database' => env('DB_CONNECTION', 'mysql'), + 'database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'job_batches', ], @@ -95,14 +96,16 @@ |-------------------------------------------------------------------------- | | These options configure the behavior of failed queue job logging so you - | can control which database and table are used to store the jobs that - | have failed. You may change them to any database / table you wish. + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" | */ '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/sanctum.php b/config/sanctum.php deleted file mode 100644 index 4fd32c77..00000000 --- a/config/sanctum.php +++ /dev/null @@ -1,85 +0,0 @@ - explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( - '%s%s', - 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1', - class_exists(Sanctum::class) - ? Sanctum::currentApplicationUrlWithPort() - : (env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : '') - ))), - - /* - |-------------------------------------------------------------------------- - | Sanctum Guards - |-------------------------------------------------------------------------- - | - | This array contains the authentication guards that will be checked when - | Sanctum is trying to authenticate a request. If none of these guards - | are able to authenticate the request, Sanctum will use the bearer - | token that's present on an incoming request for authentication. - | - */ - - 'guard' => ['web'], - - /* - |-------------------------------------------------------------------------- - | Expiration Minutes - |-------------------------------------------------------------------------- - | - | This value controls the number of minutes until an issued token will be - | considered expired. This will override any values set in the token's - | "expires_at" attribute, but first-party sessions are not affected. - | - */ - - 'expiration' => null, - - /* - |-------------------------------------------------------------------------- - | Token Prefix - |-------------------------------------------------------------------------- - | - | Sanctum can prefix new tokens in order to take advantage of various - | security scanning initiaives maintained by open source platforms - | that alert developers if they commit tokens into repositories. - | - | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning - | - */ - - 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), - - /* - |-------------------------------------------------------------------------- - | Sanctum Middleware - |-------------------------------------------------------------------------- - | - | When authenticating your first-party SPA with Sanctum you may need to - | customize some of the middleware Sanctum uses while processing the - | request. You may change the middleware listed below as required. - | - */ - - 'middleware' => [ - 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, - 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, - 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, - ], - -]; diff --git a/config/services.php b/config/services.php old mode 100644 new mode 100755 index 0ace530e..6bb68f6a --- 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'), ], @@ -31,4 +24,11 @@ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), ], + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + ]; diff --git a/config/session.php b/config/session.php old mode 100644 new mode 100755 index b65084af..6452eb72 --- a/config/session.php +++ b/config/session.php @@ -9,9 +9,9 @@ | Default Session Driver |-------------------------------------------------------------------------- | - | This option controls the default session "driver" that will be used on - | requests. By default, we will use the lightweight native driver but - | you may specify any of the other wonderful drivers provided here. + | 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" @@ -27,13 +27,14 @@ | | Here you may specify the number of minutes that you wish the session | to be allowed to remain idle before it expires. If you want them - | to immediately expire on the browser closing, set that option. + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. | */ 'lifetime' => env('SESSION_LIFETIME', 120), - 'expire_on_close' => false, + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), /* |-------------------------------------------------------------------------- @@ -41,21 +42,21 @@ |-------------------------------------------------------------------------- | | This option allows you to easily specify that all of your session data - | should be encrypted before it is stored. All encryption will be run - | automatically by the framework and you can use the Session like normal. + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. | */ - 'encrypt' => false, + 'encrypt' => env('SESSION_ENCRYPT', false), /* |-------------------------------------------------------------------------- | Session File Location |-------------------------------------------------------------------------- | - | When using the native session driver, we need a location 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. | */ @@ -79,22 +80,22 @@ | 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. | */ - 'table' => 'sessions', + 'table' => env('SESSION_TABLE', 'sessions'), /* |-------------------------------------------------------------------------- | 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" | @@ -120,15 +121,15 @@ | 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. | */ 'cookie' => env( 'SESSION_COOKIE', - Str::slug(env('APP_NAME', 'application'), '_').'_session' + Str::slug(env('APP_NAME', 'laravel'), '_').'_session' ), /* @@ -138,20 +139,20 @@ | | 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. | */ - 'path' => '/', + 'path' => env('SESSION_PATH', '/'), /* |-------------------------------------------------------------------------- | 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. | */ @@ -177,11 +178,11 @@ | | 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. | */ - 'http_only' => true, + 'http_only' => env('SESSION_HTTP_ONLY', true), /* |-------------------------------------------------------------------------- @@ -192,10 +193,25 @@ | 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 | */ - 'same_site' => 'lax', + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), ]; diff --git a/config/view.php b/config/view.php old mode 100644 new mode 100755 index 63c18d17..ee1c054f --- a/config/view.php +++ b/config/view.php @@ -79,4 +79,5 @@ 'directives' => [ // 'foo' => App\View\FooDirective::class, ], + ]; diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index c1746bfa..41a3b206 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -3,7 +3,9 @@ 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; use Illuminate\Contracts\Foundation\CachesConfiguration; use Illuminate\Contracts\Foundation\CachesRoutes; @@ -14,6 +16,7 @@ use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables; use Illuminate\Foundation\Events\LocaleUpdated; use Illuminate\Http\Request; +use Illuminate\Log\Context\ContextServiceProvider; use Illuminate\Log\LogServiceProvider; use Illuminate\Routing\RoutingServiceProvider; use Illuminate\Support\Arr; @@ -23,6 +26,8 @@ use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; use RuntimeException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutput; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -40,7 +45,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.43.0'; + const VERSION = '11.1.1'; /** * The base path for the Laravel installation. @@ -49,6 +54,13 @@ class Application extends Container implements ApplicationContract, CachesConfig */ protected $basePath; + /** + * The array of registered callbacks. + * + * @var callable[] + */ + protected $registeredCallbacks = []; + /** * Indicates if the application has been bootstrapped before. * @@ -206,6 +218,39 @@ public function __construct($basePath = null) $this->registerCoreContainerAliases(); } + /** + * Begin configuring a new Laravel application instance. + * + * @param string|null $basePath + * @return \Illuminate\Foundation\Configuration\ApplicationBuilder + */ + public static function configure(string $basePath = null) + { + $basePath = match (true) { + is_string($basePath) => $basePath, + default => static::inferBasePath(), + }; + + return (new Configuration\ApplicationBuilder(new static($basePath))) + ->withKernels() + ->withEvents() + ->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]), + }; + } + /** * Get the version number of the application. * @@ -244,6 +289,7 @@ protected function registerBaseServiceProviders() { $this->register(new EventServiceProvider($this)); $this->register(new LogServiceProvider($this)); + $this->register(new ContextServiceProvider($this)); $this->register(new RoutingServiceProvider($this)); } @@ -404,6 +450,16 @@ public function bootstrapPath($path = '') return $this->joinPaths($this->bootstrapPath, $path); } + /** + * Get the path to the service provider list in the bootstrap directory. + * + * @return string + */ + public function getBootstrapProvidersPath() + { + return $this->bootstrapPath('providers.php'); + } + /** * Set the bootstrap file directory. * @@ -749,6 +805,17 @@ public function hasDebugModeEnabled() return (bool) $this['config']->get('app.debug'); } + /** + * Register a new registered listener. + * + * @param callable $callback + * @return void + */ + public function registered($callback) + { + $this->registeredCallbacks[] = $callback; + } + /** * Register all of the configured providers. * @@ -763,6 +830,8 @@ public function registerConfiguredProviders() (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath())) ->load($providers->collapse()->toArray()); + + $this->fireAppCallbacks($this->registeredCallbacks); } /** @@ -1086,6 +1155,41 @@ public function handle(SymfonyRequest $request, int $type = self::MAIN_REQUEST, return $this[HttpKernelContract::class]->handle(Request::createFromBase($request)); } + /** + * Handle the incoming HTTP request and send the response to the browser. + * + * @param \Illuminate\Http\Request $request + * @return void + */ + public function handleRequest(Request $request) + { + $kernel = $this->make(HttpKernelContract::class); + + $response = $kernel->handle($request)->send(); + + $kernel->terminate($request, $response); + } + + /** + * Handle the incoming Artisan command. + * + * @param \Symfony\Component\Console\Input\InputInterface $input + * @return int + */ + public function handleCommand(InputInterface $input) + { + $kernel = $this->make(ConsoleKernelContract::class); + + $status = $kernel->handle( + $input, + new ConsoleOutput + ); + + $kernel->terminate($input, $status); + + return $status; + } + /** * Determine if middleware has been disabled for the application. * diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php index 31ac7e49..90bc446d 100644 --- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php +++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php @@ -9,6 +9,7 @@ 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; @@ -37,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; @@ -177,7 +178,7 @@ protected function ensureNullLogDriverIsConfigured() */ public function handleException(Throwable $e) { - self::$reservedMemory = null; + static::$reservedMemory = null; try { $this->getExceptionHandler()->report($e); @@ -225,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)); @@ -292,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/LoadConfiguration.php b/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php index ae3f7388..1d26b960 100644 --- a/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php +++ b/src/Illuminate/Foundation/Bootstrap/LoadConfiguration.php @@ -27,7 +27,7 @@ public function bootstrap(Application $app) if (file_exists($cached = $app->getCachedConfigPath())) { $items = require $cached; - $loadedFromCache = true; + $app->instance('config_loaded_from_cache', $loadedFromCache = true); } // Next we will spin through all of the configuration files in the configuration @@ -62,13 +62,69 @@ protected function loadConfigurationFiles(Application $app, RepositoryContract $ { $files = $this->getConfigurationFiles($app); - if (! isset($files['app'])) { - throw new Exception('Unable to load the "app" configuration file.'); + // if (! isset($files['app'])) { + // throw new Exception('Unable to load the "app" configuration file.'); + // } + + $base = $this->getBaseConfiguration(); + + foreach ($files as $name => $path) { + $base = $this->loadConfigurationFile($repository, $name, $path, $base); } - foreach ($files as $key => $path) { - $repository->set($key, require $path); + foreach ($base as $name => $config) { + $repository->set($name, $config); + } + } + + /** + * Load the given configuration file. + * + * @param \Illuminate\Contracts\Config\Repository $repository + * @param string $name + * @param string $path + * @param array $base + * @return array + */ + protected function loadConfigurationFile(RepositoryContract $repository, $name, $path, array $base) + { + $config = require $path; + + if (isset($base[$name])) { + $config = array_merge($base[$name], $config); + + foreach ($this->mergeableOptions($name) as $option) { + if (isset($config[$option])) { + $config[$option] = array_merge($base[$name][$option], $config[$option]); + } + } + + unset($base[$name]); } + + $repository->set($name, $config); + + return $base; + } + + /** + * Get the options within the configuration file that should be merged again. + * + * @param string $name + * @return array + */ + protected function mergeableOptions($name) + { + return [ + 'auth' => ['guards', 'providers', 'passwords'], + 'broadcasting' => ['connections'], + 'cache' => ['stores'], + 'database' => ['connections'], + 'filesystems' => ['disks'], + 'logging' => ['channels'], + 'mail' => ['mailers'], + 'queue' => ['connections'], + ][$name] ?? []; } /** @@ -83,6 +139,10 @@ protected function getConfigurationFiles(Application $app) $configPath = realpath($app->configPath()); + if (! $configPath) { + return []; + } + foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) { $directory = $this->getNestedDirectory($file, $configPath); @@ -111,4 +171,20 @@ protected function getNestedDirectory(SplFileInfo $file, $configPath) return $nested; } + + /** + * Get the base configuration files. + * + * @return array + */ + protected function getBaseConfiguration() + { + $config = []; + + foreach (Finder::create()->files()->name('*.php')->in(__DIR__.'/../../../../config') as $file) { + $config[basename($file->getRealPath(), '.php')] = require $file->getRealPath(); + } + + return $config; + } } diff --git a/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php b/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php index 3f0be6c0..050a9696 100644 --- a/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php +++ b/src/Illuminate/Foundation/Bootstrap/LoadEnvironmentVariables.php @@ -94,7 +94,7 @@ protected function createDotenv($app) * Write the error information to the screen and exit. * * @param \Dotenv\Exception\InvalidFileException $e - * @return void + * @return never */ protected function writeErrorAndDie(InvalidFileException $e) { diff --git a/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php b/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php index f18375cf..70065191 100644 --- a/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php +++ b/src/Illuminate/Foundation/Bootstrap/RegisterProviders.php @@ -6,6 +6,20 @@ class RegisterProviders { + /** + * The service providers that should be merged before registration. + * + * @var array + */ + protected static $merge = []; + + /** + * The path to the bootstrap provider configuration file. + * + * @var string|null + */ + protected static $bootstrapProviderPath; + /** * Bootstrap the given application. * @@ -14,6 +28,67 @@ class RegisterProviders */ public function bootstrap(Application $app) { + if (! $app->bound('config_loaded_from_cache') || + $app->make('config_loaded_from_cache') === false) { + $this->mergeAdditionalProviders($app); + } + $app->registerConfiguredProviders(); } + + /** + * Merge the additional configured providers into the configuration. + * + * @param \Illuminate\Foundation\Application $app + */ + protected function mergeAdditionalProviders(Application $app) + { + if (static::$bootstrapProviderPath && + file_exists(static::$bootstrapProviderPath)) { + $packageProviders = require static::$bootstrapProviderPath; + + foreach ($packageProviders as $index => $provider) { + if (! class_exists($provider)) { + unset($packageProviders[$index]); + } + } + } + + $app->make('config')->set( + 'app.providers', + array_merge( + $app->make('config')->get('app.providers'), + static::$merge, + array_values($packageProviders ?? []), + ), + ); + } + + /** + * Merge the given providers into the provider configuration before registration. + * + * @param array $providers + * @param string|null $bootstrapProviderPath + * @return void + */ + public static function merge(array $providers, ?string $bootstrapProviderPath = null) + { + static::$bootstrapProviderPath = $bootstrapProviderPath; + + static::$merge = array_values(array_filter(array_unique( + 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/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php new file mode 100644 index 00000000..4f4469fe --- /dev/null +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -0,0 +1,403 @@ +app->singleton( + \Illuminate\Contracts\Http\Kernel::class, + \Illuminate\Foundation\Http\Kernel::class, + ); + + $this->app->singleton( + \Illuminate\Contracts\Console\Kernel::class, + \Illuminate\Foundation\Console\Kernel::class, + ); + + return $this; + } + + /** + * Register additional service providers. + * + * @param array $providers + * @param bool $withBootstrapProviders + * @return $this + */ + public function withProviders(array $providers = [], bool $withBootstrapProviders = true) + { + RegisterProviders::merge( + $providers, + $withBootstrapProviders + ? $this->app->getBootstrapProvidersPath() + : null + ); + + return $this; + } + + /** + * Register the core event service provider for the application. + * + * @param array $discover + * @return $this + */ + public function withEvents(array $discover = []) + { + 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; + } + + /** + * Register the braodcasting services for the application. + * + * @param string $channels + * @param array $attributes + * @return $this + */ + public function withBroadcasting(string $channels, array $attributes = []) + { + $this->app->booted(function () use ($channels, $attributes) { + Broadcast::routes(! empty($attributes) ? $attributes : null); + + if (file_exists($channels)) { + require $channels; + } + }); + + return $this; + } + + /** + * Register the routing services for the application. + * + * @param \Closure|null $using + * @param string|null $web + * @param string|null $api + * @param string|null $commands + * @param string|null $channels + * @param string|null $pages + * @param string $apiPrefix + * @param callable|null $then + * @return $this + */ + public function withRouting(?Closure $using = null, + ?string $web = null, + ?string $api = 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) || 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, force: true); + }); + + if (is_string($commands) && realpath($commands) !== false) { + $this->withCommands([$commands]); + } + + if (is_string($channels) && realpath($channels) !== false) { + $this->withBroadcasting($channels); + } + + return $this; + } + + /** + * Create the routing callback for the application. + * + * @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 + */ + protected function buildRoutingCallback(?string $web, + ?string $api, + ?string $pages, + ?string $health, + string $apiPrefix, + ?callable $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); + } + + if (is_string($pages) && + realpath($pages) !== false && + class_exists(Folio::class)) { + Folio::route($pages, middleware: $this->pageMiddleware); + } + + if (is_callable($then)) { + $then($this->app); + } + }; + } + + /** + * Register the global middleware, middleware groups, and middleware aliases for the application. + * + * @param callable|null $callback + * @return $this + */ + public function withMiddleware(?callable $callback = null) + { + $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) { + $middleware = (new Middleware) + ->redirectGuestsTo(fn () => route('login')); + + 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; + } + + /** + * Register additional Artisan commands with the application. + * + * @param array $commands + * @return $this + */ + public function withCommands(array $commands = []) + { + if (empty($commands)) { + $commands = [$this->app->path('Console/Commands')]; + } + + $this->app->afterResolving(ConsoleKernel::class, function ($kernel) use ($commands) { + [$commands, $paths] = collect($commands)->partition(fn ($command) => class_exists($command)); + [$routes, $paths] = $paths->partition(fn ($path) => is_file($path)); + + $this->app->booted(static function () use ($kernel, $commands, $paths, $routes) { + $kernel->addCommands($commands->all()); + $kernel->addCommandPaths($paths->all()); + $kernel->addCommandRoutePaths($routes->all()); + }); + }); + + return $this; + } + + /** + * Register additional Artisan route paths. + * + * @param array $paths + * @return $this + */ + protected function withCommandRouting(array $paths) + { + $this->app->afterResolving(ConsoleKernel::class, function ($kernel) use ($paths) { + $this->app->booted(fn () => $kernel->addCommandRoutePaths($paths)); + }); + } + + /** + * Register the scheduled tasks for the application. + * + * @param callable(Schedule $schedule): void $callback + * @return $this + */ + public function withSchedule(callable $callback) + { + Artisan::starting(fn () => $callback($this->app->make(Schedule::class))); + + return $this; + } + + /** + * Register and configure the application's exception handler. + * + * @param callable|null $using + * @return $this + */ + public function withExceptions(?callable $using = null) + { + $this->app->singleton( + \Illuminate\Contracts\Debug\ExceptionHandler::class, + \Illuminate\Foundation\Exceptions\Handler::class + ); + + $using ??= fn () => true; + + $this->app->afterResolving( + \Illuminate\Foundation\Exceptions\Handler::class, + fn ($handler) => $using(new Exceptions($handler)), + ); + + return $this; + } + + /** + * Register an array of container bindings to be bound when the application is booting. + * + * @param array $bindings + * @return $this + */ + public function withBindings(array $bindings) + { + return $this->registered(function ($app) use ($bindings) { + foreach ($bindings as $abstract => $concrete) { + $app->bind($abstract, $concrete); + } + }); + } + + /** + * Register an array of singleton container bindings to be bound when the application is booting. + * + * @param array $singletons + * @return $this + */ + public function withSingletons(array $singletons) + { + return $this->registered(function ($app) use ($singletons) { + foreach ($singletons as $abstract => $concrete) { + if (is_string($abstract)) { + $app->singleton($abstract, $concrete); + } else { + $app->singleton($concrete); + } + } + }); + } + + /** + * 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". + * + * @param callable $callback + * @return $this + */ + public function booting(callable $callback) + { + $this->app->booting($callback); + + return $this; + } + + /** + * Register a callback to be invoked when the application is "booted". + * + * @param callable $callback + * @return $this + */ + public function booted(callable $callback) + { + $this->app->booted($callback); + + return $this; + } + + /** + * Get the application instance. + * + * @return \Illuminate\Foundation\Application + */ + public function create() + { + return $this->app; + } +} diff --git a/src/Illuminate/Foundation/Configuration/Exceptions.php b/src/Illuminate/Foundation/Configuration/Exceptions.php new file mode 100644 index 00000000..532cb06d --- /dev/null +++ b/src/Illuminate/Foundation/Configuration/Exceptions.php @@ -0,0 +1,203 @@ +handler->reportable($using); + } + + /** + * Register a reportable callback. + * + * @param callable $reportUsing + * @return \Illuminate\Foundation\Exceptions\ReportableHandler + */ + public function reportable(callable $reportUsing) + { + return $this->handler->reportable($reportUsing); + } + + /** + * Register a renderable callback. + * + * @param callable $using + * @return $this + */ + public function render(callable $using) + { + $this->handler->renderable($using); + + return $this; + } + + /** + * Register a renderable callback. + * + * @param callable $renderUsing + * @return $this + */ + public function renderable(callable $renderUsing) + { + $this->handler->renderable($renderUsing); + + return $this; + } + + /** + * Register a callback to prepare the final, rendered exception response. + * + * @param callable $using + * @return $this + */ + public function respond(callable $using) + { + $this->handler->respondUsing($using); + + return $this; + } + + /** + * Specify the callback that should be used to throttle reportable exceptions. + * + * @param callable $throttleUsing + * @return $this + */ + public function throttle(callable $throttleUsing) + { + $this->handler->throttleUsing($throttleUsing); + + return $this; + } + + /** + * Register a new exception mapping. + * + * @param \Closure|string $from + * @param \Closure|string|null $to + * @return $this + * + * @throws \InvalidArgumentException + */ + public function map($from, $to = null) + { + $this->handler->map($from, $to); + + return $this; + } + + /** + * Set the log level for the given exception type. + * + * @param class-string<\Throwable> $type + * @param \Psr\Log\LogLevel::* $level + * @return $this + */ + public function level(string $type, string $level) + { + $this->handler->level($type, $level); + + return $this; + } + + /** + * Register a closure that should be used to build exception context data. + * + * @param \Closure $contextCallback + * @return $this + */ + public function context(Closure $contextCallback) + { + $this->handler->buildContextUsing($contextCallback); + + return $this; + } + + /** + * Indicate that the given exception type should not be reported. + * + * @param array|string $class + * @return $this + */ + public function dontReport(array|string $class) + { + foreach (Arr::wrap($class) as $exceptionClass) { + $this->handler->dontReport($exceptionClass); + } + + return $this; + } + + /** + * Do not report duplicate exceptions. + * + * @return $this + */ + public function dontReportDuplicates() + { + $this->handler->dontReportDuplicates(); + + return $this; + } + + /** + * Indicate that the given attributes should never be flashed to the session on validation errors. + * + * @param array|string $attributes + * @return $this + */ + public function dontFlash(array|string $attributes) + { + $this->handler->dontFlash($attributes); + + return $this; + } + + /** + * Register the callable that determines if the exception handler response should be JSON. + * + * @param callable(\Illuminate\Http\Request $request, \Throwable): bool $callback + * @return $this + */ + public function shouldRenderJsonWhen(callable $callback) + { + $this->handler->shouldRenderJsonWhen($callback); + + 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 new file mode 100644 index 00000000..e8916d55 --- /dev/null +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -0,0 +1,772 @@ +prepends = array_merge( + Arr::wrap($middleware), + $this->prepends + ); + + return $this; + } + + /** + * Append middleware to the application's global middleware stack. + * + * @param array|string $middleware + * @return $this + */ + public function append(array|string $middleware) + { + $this->appends = array_merge( + $this->appends, + Arr::wrap($middleware) + ); + + return $this; + } + + /** + * Remove middleware from the application's global middleware stack. + * + * @param array|string $middleware + * @return $this + */ + public function remove(array|string $middleware) + { + $this->removals = array_merge( + $this->removals, + Arr::wrap($middleware) + ); + + return $this; + } + + /** + * Specify a middleware that should be replaced with another middleware. + * + * @param string $search + * @param string $replace + * @return $this + */ + public function replace(string $search, string $replace) + { + $this->replacements[$search] = $replace; + + return $this; + } + + /** + * Define the global middleware for the application. + * + * @param array $middleware + * @return $this + */ + public function use(array $middleware) + { + $this->global = $middleware; + + return $this; + } + + /** + * Define a middleware group. + * + * @param string $group + * @param array $middleware + * @return $this + */ + public function group(string $group, array $middleware) + { + $this->groups[$group] = $middleware; + + return $this; + } + + /** + * Prepend the given middleware to the specified group. + * + * @param string $group + * @param array|string $middleware + * @return $this + */ + public function prependToGroup(string $group, array|string $middleware) + { + $this->groupPrepends[$group] = array_merge( + Arr::wrap($middleware), + $this->groupPrepends[$group] ?? [] + ); + + return $this; + } + + /** + * Append the given middleware to the specified group. + * + * @param string $group + * @param array|string $middleware + * @return $this + */ + public function appendToGroup(string $group, array|string $middleware) + { + $this->groupAppends[$group] = array_merge( + $this->groupAppends[$group] ?? [], + Arr::wrap($middleware) + ); + + return $this; + } + + /** + * Remove the given middleware from the specified group. + * + * @param string $group + * @param array|string $middleware + * @return $this + */ + public function removeFromGroup(string $group, array|string $middleware) + { + $this->groupRemovals[$group] = array_merge( + Arr::wrap($middleware), + $this->groupRemovals[$group] ?? [] + ); + + return $this; + } + + /** + * Replace the given middleware in the specified group with another middleware. + * + * @param string $group + * @param string $search + * @param string $replace + * @return $this + */ + public function replaceInGroup(string $group, string $search, string $replace) + { + $this->groupReplacements[$group][$search] = $replace; + + return $this; + } + + /** + * Modify the middleware in the "web" group. + * + * @param string $group + * @param array|string $append + * @param array|string $prepend + * @param array|string $remove + * @param array $replace + * @return $this + */ + public function web(array|string $append = [], array|string $prepend = [], array|string $remove = [], array $replace = []) + { + return $this->modifyGroup('web', $append, $prepend, $remove, $replace); + } + + /** + * Modify the middleware in the "api" group. + * + * @param string $group + * @param array|string $append + * @param array|string $prepend + * @param array|string $remove + * @param array $replace + * @return $this + */ + public function api(array|string $append = [], array|string $prepend = [], array|string $remove = [], array $replace = []) + { + return $this->modifyGroup('api', $append, $prepend, $remove, $replace); + } + + /** + * Modify the middleware in the given group. + * + * @param string $group + * @param array|string $append + * @param array|string $prepend + * @param array|string $remove + * @param array $replace + * @return $this + */ + protected function modifyGroup(string $group, array|string $append, array|string $prepend, array|string $remove, array $replace) + { + if (! empty($append)) { + $this->appendToGroup($group, $append); + } + + if (! empty($prepend)) { + $this->prependToGroup($group, $prepend); + } + + if (! empty($remove)) { + $this->removeFromGroup($group, $remove); + } + + if (! empty($replace)) { + foreach ($replace as $search => $replace) { + $this->replaceInGroup($group, $search, $replace); + } + } + + return $this; + } + + /** + * Register the Folio / page middleware for the application. + * + * @param array $middleware + * @return $this + */ + public function pages(array $middleware) + { + $this->pageMiddleware = $middleware; + + return $this; + } + + /** + * Register additional middleware aliases. + * + * @param array $aliases + * @return $this + */ + public function alias(array $aliases) + { + $this->customAliases = $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. + * + * @return array + */ + public function getGlobalMiddleware() + { + $middleware = $this->global ?: array_values(array_filter([ + $this->trustHosts ? \Illuminate\Http\Middleware\TrustHosts::class : null, + \Illuminate\Http\Middleware\TrustProxies::class, + \Illuminate\Http\Middleware\HandleCors::class, + \Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance::class, + \Illuminate\Http\Middleware\ValidatePostSize::class, + \Illuminate\Foundation\Http\Middleware\TrimStrings::class, + \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ])); + + $middleware = array_map(function ($middleware) { + return isset($this->replacements[$middleware]) + ? $this->replacements[$middleware] + : $middleware; + }, $middleware); + + return array_values(array_filter( + array_diff( + array_unique(array_merge($this->prepends, $middleware, $this->appends)), + $this->removals + ) + )); + } + + /** + * Get the middleware groups. + * + * @return array + */ + public function getMiddlewareGroups() + { + $middleware = [ + '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, + $this->apiLimiter ? 'throttle:'.$this->apiLimiter : null, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ])), + ]; + + $middleware = array_merge($middleware, $this->groups); + + foreach ($middleware as $group => $groupedMiddleware) { + foreach ($groupedMiddleware as $index => $groupMiddleware) { + if (isset($this->groupReplacements[$group][$groupMiddleware])) { + $middleware[$group][$index] = $this->groupReplacements[$group][$groupMiddleware]; + } + } + } + + foreach ($this->groupRemovals as $group => $removals) { + $middleware[$group] = array_values(array_filter( + array_diff($middleware[$group] ?? [], $removals) + )); + } + + foreach ($this->groupPrepends as $group => $prepends) { + $middleware[$group] = array_values(array_filter( + array_unique(array_merge($prepends, $middleware[$group] ?? [])) + )); + } + + foreach ($this->groupAppends as $group => $appends) { + $middleware[$group] = array_values(array_filter( + array_unique(array_merge($middleware[$group] ?? [], $appends)) + )); + } + + return $middleware; + } + + /** + * Configure where guests are redirected by the "auth" 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 "guest" middleware. + * + * @param callable|string $redirect + * @return $this + */ + public function redirectUsersTo(callable|string $redirect) + { + return $this->redirectTo(users: $redirect); + } + + /** + * Configure where users are redirected by the authentication and guest middleware. + * + * @param callable|string $guests + * @param callable|string $users + * @return $this + */ + public function redirectTo(callable|string $guests = null, callable|string $users = null) + { + $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 CSRF token validation middleware. + * + * @param array $except + * @return $this + */ + public function validateCsrfTokens(array $except = []) + { + 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; + } + + /** + * Indicate that the trusted host middleware should be enabled. + * + * @param array|null $at + * @param bool $subdomains + * @return $this + */ + 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; + } + + /** + * Indicate that Sanctum's frontend state middleware should be enabled. + * + * @return $this + */ + public function statefulApi() + { + $this->statefulApi = true; + + return $this; + } + + /** + * Indicate that the API middleware group's throttling middleware should be enabled. + * + * @param string $limiter + * @param bool $redis + * @return $this + */ + public function throttleApi($limiter = 'api', $redis = false) + { + $this->apiLimiter = $limiter; + + if ($redis) { + $this->throttleWithRedis(); + } + + return $this; + } + + /** + * Indicate that Laravel's throttling middleware should use Redis. + * + * @return $this + */ + public function throttleWithRedis() + { + $this->throttleWithRedis = true; + + 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. + * + * @return array + */ + public function getPageMiddleware() + { + return $this->pageMiddleware; + } + + /** + * Get the middleware aliases. + * + * @return array + */ + public function getMiddlewareAliases() + { + return array_merge($this->defaultAliases(), $this->customAliases); + } + + /** + * Get the default middleware aliases. + * + * @return array + */ + protected function defaultAliases() + { + $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, + 'throttle' => $this->throttleWithRedis + ? \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class + : \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; + + if (class_exists(\Spark\Http\Middleware\VerifyBillableIsSubscribed::class)) { + $aliases['subscribed'] = \Spark\Http\Middleware\VerifyBillableIsSubscribed::class; + } + + return $aliases; + } + + /** + * Get the middleware priority for the application. + * + * @return array + */ + public function getMiddlewarePriority() + { + return $this->priority; + } +} diff --git a/src/Illuminate/Foundation/Console/ApiInstallCommand.php b/src/Illuminate/Foundation/Console/ApiInstallCommand.php new file mode 100644 index 00000000..dda72efc --- /dev/null +++ b/src/Illuminate/Foundation/Console/ApiInstallCommand.php @@ -0,0 +1,153 @@ +option('passport')) { + $this->installPassport(); + } else { + $this->installSanctum(); + } + + if (file_exists($apiRoutesPath = $this->laravel->basePath('routes/api.php')) && + ! $this->option('force')) { + $this->components->error('API routes file already exists.'); + } else { + $this->components->info('Published API routes file.'); + + copy(__DIR__.'/stubs/api-routes.stub', $apiRoutesPath); + + if ($this->option('passport')) { + (new Filesystem)->replaceInFile( + 'auth:sanctum', + 'auth:api', + $apiRoutesPath, + ); + } + + $this->uncommentApiRoutesFile(); + } + + if ($this->option('passport')) { + Process::run(array_filter([ + (new PhpExecutableFinder())->find(false) ?: 'php', + defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan', + 'passport:install', + $this->confirm('Would you like to use UUIDs for all client IDs?') ? '--uuids' : null, + ])); + + $this->components->info('API scaffolding installed. Please add the [Laravel\Passport\HasApiTokens] trait to your User model.'); + } else { + if (! $this->option('without-migration-prompt')) { + if ($this->confirm('One new database migration has been published. Would you like to run all pending database migrations?', true)) { + $this->call('migrate'); + } + } + + $this->components->info('API scaffolding installed. Please add the [Laravel\Sanctum\HasApiTokens] trait to your User model.'); + } + } + + /** + * Uncomment the API routes file in the application bootstrap file. + * + * @return void + */ + protected function uncommentApiRoutesFile() + { + $appBootstrapPath = $this->laravel->bootstrapPath('app.php'); + + $content = file_get_contents($appBootstrapPath); + + if (str_contains($content, '// api: ')) { + (new Filesystem)->replaceInFile( + '// api: ', + 'api: ', + $appBootstrapPath, + ); + } elseif (str_contains($content, 'web: __DIR__.\'/../routes/web.php\',')) { + (new Filesystem)->replaceInFile( + 'web: __DIR__.\'/../routes/web.php\',', + 'web: __DIR__.\'/../routes/web.php\','.PHP_EOL.' api: __DIR__.\'/../routes/api.php\',', + $appBootstrapPath, + ); + } else { + $this->components->warn('Unable to automatically add API route definition to bootstrap file. API route file should be registered manually.'); + + return; + } + } + + /** + * Install Laravel Sanctum into the application. + * + * @return void + */ + protected function installSanctum() + { + $this->requireComposerPackages($this->option('composer'), [ + 'laravel/sanctum:^4.0', + ]); + + $migrationPublished = collect(scandir($this->laravel->databasePath('migrations')))->contains(function ($migration) { + return preg_match('/\d{4}_\d{2}_\d{2}_\d{6}_create_personal_access_tokens_table.php/', $migration); + }); + + if (! $migrationPublished) { + Process::run([ + (new PhpExecutableFinder())->find(false) ?: 'php', + defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan', + 'vendor:publish', + '--provider', + 'Laravel\\Sanctum\\SanctumServiceProvider', + ]); + } + } + + /** + * Install Laravel Passport into the application. + * + * @return void + */ + protected function installPassport() + { + $this->requireComposerPackages($this->option('composer'), [ + 'laravel/passport:^12.0', + ]); + } +} diff --git a/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php new file mode 100644 index 00000000..9eb09793 --- /dev/null +++ b/src/Illuminate/Foundation/Console/BroadcastingInstallCommand.php @@ -0,0 +1,209 @@ +call('config:publish', ['name' => 'broadcasting']); + + // Install channel routes file... + if (! file_exists($broadcastingRoutesPath = $this->laravel->basePath('routes/channels.php')) || $this->option('force')) { + $this->components->info("Published 'channels' route file."); + + copy(__DIR__.'/stubs/broadcasting-routes.stub', $broadcastingRoutesPath); + } + + $this->uncommentChannelsRoutesFile(); + $this->enableBroadcastServiceProvider(); + + // Install bootstrapping... + if (! file_exists($echoScriptPath = $this->laravel->resourcePath('js/echo.js'))) { + if (! is_dir($directory = $this->laravel->resourcePath('js'))) { + mkdir($directory, 0755, true); + } + + copy(__DIR__.'/stubs/echo-js.stub', $echoScriptPath); + } + + if (file_exists($bootstrapScriptPath = $this->laravel->resourcePath('js/bootstrap.js'))) { + $bootstrapScript = file_get_contents( + $bootstrapScriptPath + ); + + if (! str_contains($bootstrapScript, './echo')) { + file_put_contents( + $bootstrapScriptPath, + trim($bootstrapScript.PHP_EOL.file_get_contents(__DIR__.'/stubs/echo-bootstrap-js.stub')).PHP_EOL, + ); + } + } + + $this->installReverb(); + + $this->installNodeDependencies(); + } + + /** + * Uncomment the "channels" routes file in the application bootstrap file. + * + * @return void + */ + protected function uncommentChannelsRoutesFile() + { + $appBootstrapPath = $this->laravel->bootstrapPath('app.php'); + + $content = file_get_contents($appBootstrapPath); + + if (str_contains($content, '// channels: ')) { + (new Filesystem)->replaceInFile( + '// channels: ', + 'channels: ', + $appBootstrapPath, + ); + } elseif (str_contains($content, 'channels: ')) { + return; + } elseif (str_contains($content, 'commands: __DIR__.\'/../routes/console.php\',')) { + (new Filesystem)->replaceInFile( + 'commands: __DIR__.\'/../routes/console.php\',', + 'commands: __DIR__.\'/../routes/console.php\','.PHP_EOL.' channels: __DIR__.\'/../routes/channels.php\',', + $appBootstrapPath, + ); + } + } + + /** + * Uncomment the "BroadcastServiceProvider" in the application configuration. + * + * @return void + */ + protected function enableBroadcastServiceProvider() + { + $config = ($filesystem = new Filesystem)->get(app()->configPath('app.php')); + + if (str_contains($config, '// App\Providers\BroadcastServiceProvider::class')) { + $filesystem->replaceInFile( + '// App\Providers\BroadcastServiceProvider::class', + 'App\Providers\BroadcastServiceProvider::class', + app()->configPath('app.php'), + ); + } + } + + /** + * Install Laravel Reverb into the application if desired. + * + * @return void + */ + protected function installReverb() + { + if ($this->option('without-reverb') || 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.'); + } + + /** + * Install and build Node dependencies. + * + * @return void + */ + protected function installNodeDependencies() + { + if ($this->option('without-node') || ! confirm('Would you like to install and build the Node dependencies required for broadcasting?', default: true)) { + return; + } + + $this->components->info('Installing and building Node dependencies.'); + + if (file_exists(base_path('pnpm-lock.yaml'))) { + $commands = [ + 'pnpm add --save-dev laravel-echo pusher-js', + 'pnpm run build', + ]; + } elseif (file_exists(base_path('yarn.lock'))) { + $commands = [ + 'yarn add --dev laravel-echo pusher-js', + 'yarn run build', + ]; + } elseif (file_exists(base_path('bun.lockb'))) { + $commands = [ + 'bun add --dev laravel-echo pusher-js', + 'bun run build', + ]; + } else { + $commands = [ + 'npm install --save-dev laravel-echo pusher-js', + 'npm run build', + ]; + } + + $command = Process::command(implode(' && ', $commands)) + ->path(base_path()); + + if (! windows_os()) { + $command->tty(true); + } + + if ($command->run()->failed()) { + $this->components->warn("Node dependency installation failed. Please run the following commands manually: \n\n".implode(' && ', $commands)); + } else { + $this->components->info('Node dependencies installed successfully.'); + } + } +} diff --git a/src/Illuminate/Foundation/Console/ChannelListCommand.php b/src/Illuminate/Foundation/Console/ChannelListCommand.php index 8e4d8bac..7063f1ad 100644 --- a/src/Illuminate/Foundation/Console/ChannelListCommand.php +++ b/src/Illuminate/Foundation/Console/ChannelListCommand.php @@ -36,7 +36,7 @@ class ChannelListCommand extends Command /** * Execute the console command. * - * @param \Illuminate\Contracts\Broadcasting\Broadcaster + * @param \Illuminate\Contracts\Broadcasting\Broadcaster $broadcaster * @return void */ public function handle(Broadcaster $broadcaster) diff --git a/src/Illuminate/Foundation/Console/ClassMakeCommand.php b/src/Illuminate/Foundation/Console/ClassMakeCommand.php new file mode 100644 index 00000000..5c091c60 --- /dev/null +++ b/src/Illuminate/Foundation/Console/ClassMakeCommand.php @@ -0,0 +1,81 @@ +option('invokable') + ? $this->resolveStubPath('/stubs/class.invokable.stub') + : $this->resolveStubPath('/stubs/class.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$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 304dfcb0..6f5fd9a4 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/ClosureCommand.php b/src/Illuminate/Foundation/Console/ClosureCommand.php index 4cd54e8e..ae51801f 100644 --- a/src/Illuminate/Foundation/Console/ClosureCommand.php +++ b/src/Illuminate/Foundation/Console/ClosureCommand.php @@ -4,12 +4,19 @@ use Closure; use Illuminate\Console\Command; +use Illuminate\Support\Facades\Schedule; +use Illuminate\Support\Traits\ForwardsCalls; use ReflectionFunction; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +/** + * @mixin \Illuminate\Console\Scheduling\Event + */ class ClosureCommand extends Command { + use ForwardsCalls; + /** * The command callback. * @@ -39,7 +46,7 @@ public function __construct($signature, Closure $callback) * @param \Symfony\Component\Console\Output\OutputInterface $output * @return int */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $inputs = array_merge($input->getArguments(), $input->getOptions()); @@ -79,4 +86,29 @@ public function describe($description) return $this; } + + /** + * Create a new scheduled event for the command. + * + * @param array $parameters + * @return \Illuminate\Console\Scheduling\Event + */ + public function schedule($parameters = []) + { + return Schedule::command($this->name, $parameters); + } + + /** + * Dynamically proxy calls to a new scheduled event. + * + * @param string $method + * @param array $parameters + * @return mixed + * + * @throws \BadMethodCallException + */ + public function __call($method, $parameters) + { + return $this->forwardCallTo($this->schedule(), $method, $parameters); + } } diff --git a/src/Illuminate/Foundation/Console/ConfigPublishCommand.php b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php new file mode 100644 index 00000000..54ebce46 --- /dev/null +++ b/src/Illuminate/Foundation/Console/ConfigPublishCommand.php @@ -0,0 +1,102 @@ +getBaseConfigurationFiles(); + + if (is_null($this->argument('name')) && $this->option('all')) { + foreach ($config as $key => $file) { + $this->publish($key, $file, $this->laravel->configPath().'/'.$key.'.php'); + } + + return; + } + + $name = (string) (is_null($this->argument('name')) ? select( + label: 'Which configuration file would you like to publish?', + options: collect($config)->map(function (string $path) { + return basename($path, '.php'); + }), + ) : $this->argument('name')); + + if (! is_null($name) && ! isset($config[$name])) { + $this->components->error('Unrecognized configuration file.'); + + return 1; + } + + $this->publish($name, $config[$name], $this->laravel->configPath().'/'.$name.'.php'); + } + + /** + * Publish the given file to the given destination. + * + * @param string $name + * @param string $file + * @param string $destination + * @return void + */ + protected function publish(string $name, string $file, string $destination) + { + if (file_exists($destination) && ! $this->option('force')) { + $this->components->error("The '{$name}' configuration file already exists."); + + return; + } + + copy($file, $destination); + + $this->components->info("Published '{$name}' configuration file."); + } + + /** + * Get an array containing the base configuration files. + * + * @return array + */ + protected function getBaseConfigurationFiles() + { + $config = []; + + foreach (Finder::create()->files()->name('*.php')->in(__DIR__.'/../../../../config') as $file) { + $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 00000000..a7cfb87c --- /dev/null +++ b/src/Illuminate/Foundation/Console/EnumMakeCommand.php @@ -0,0 +1,137 @@ +option('string') || $this->option('int')) { + return $this->resolveStubPath('/stubs/enum.backed.stub'); + } + + return $this->resolveStubPath('/stubs/enum.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$stub; + } + + /** + * Get the default namespace for the class. + * + * @param string $rootNamespace + * @return string + */ + protected function getDefaultNamespace($rootNamespace) + { + return match (true) { + is_dir(app_path('Enums')) => $rootNamespace.'\\Enums', + is_dir(app_path('Enumerations')) => $rootNamespace.'\\Enumerations', + default => $rootNamespace, + }; + } + + /** + * 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/EnvironmentDecryptCommand.php b/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php index f3c3fa2c..a173388f 100644 --- a/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php +++ b/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php @@ -73,7 +73,7 @@ public function handle() $key = $this->parseKey($key); $encryptedFile = ($this->option('env') - ? base_path('.env').'.'.$this->option('env') + ? Str::finish(dirname($this->laravel->environmentFilePath()), DIRECTORY_SEPARATOR).'.env.'.$this->option('env') : $this->laravel->environmentFilePath()).'.encrypted'; $outputFile = $this->outputFilePath(); @@ -138,7 +138,7 @@ protected function parseKey(string $key) */ protected function outputFilePath() { - $path = Str::finish($this->option('path') ?: base_path(), DIRECTORY_SEPARATOR); + $path = Str::finish($this->option('path') ?: dirname($this->laravel->environmentFilePath()), DIRECTORY_SEPARATOR); $outputFile = $this->option('filename') ?: ('.env'.($this->option('env') ? '.'.$this->option('env') : '')); $outputFile = ltrim($outputFile, DIRECTORY_SEPARATOR); diff --git a/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php b/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php index 7c7b1113..f2c55fd3 100644 --- a/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php +++ b/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php @@ -64,7 +64,7 @@ public function handle() $keyPassed = $key !== null; $environmentFile = $this->option('env') - ? base_path('.env').'.'.$this->option('env') + ? Str::finish(dirname($this->laravel->environmentFilePath()), DIRECTORY_SEPARATOR).'.env.'.$this->option('env') : $this->laravel->environmentFilePath(); $encryptedFile = $environmentFile.'.encrypted'; diff --git a/src/Illuminate/Foundation/Console/EventGenerateCommand.php b/src/Illuminate/Foundation/Console/EventGenerateCommand.php index 5b0857a3..ad99e937 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 7cf7faaf..bcd82ab0 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/InteractsWithComposerPackages.php b/src/Illuminate/Foundation/Console/InteractsWithComposerPackages.php new file mode 100644 index 00000000..ebd8b7e7 --- /dev/null +++ b/src/Illuminate/Foundation/Console/InteractsWithComposerPackages.php @@ -0,0 +1,44 @@ +phpBinary(), $composer, 'require']; + } + + $command = array_merge( + $command ?? ['composer', 'require'], + $packages, + ); + + return ! (new Process($command, $this->laravel->basePath(), ['COMPOSER_MEMORY_LIMIT' => '-1'])) + ->setTimeout(null) + ->run(function ($type, $output) { + $this->output->write($output); + }); + } + + /** + * Get the path to the appropriate PHP binary. + * + * @return string + */ + protected function phpBinary() + { + return (new PhpExecutableFinder())->find(false) ?: 'php'; + } +} diff --git a/src/Illuminate/Foundation/Console/InterfaceMakeCommand.php b/src/Illuminate/Foundation/Console/InterfaceMakeCommand.php new file mode 100644 index 00000000..f2dcf7d8 --- /dev/null +++ b/src/Illuminate/Foundation/Console/InterfaceMakeCommand.php @@ -0,0 +1,65 @@ +app->runningUnitTests()) { $this->rerouteSymfonyCommandEvents(); } - - $this->defineConsoleSchedule(); }); } @@ -156,30 +175,6 @@ public function rerouteSymfonyCommandEvents() return $this; } - /** - * Define the application's command schedule. - * - * @return void - */ - protected function defineConsoleSchedule() - { - $this->app->singleton(Schedule::class, function ($app) { - return tap(new Schedule($this->scheduleTimezone()), function ($schedule) { - $this->schedule($schedule->useCache($this->scheduleCache())); - }); - }); - } - - /** - * Get the name of the cache store that should manage scheduling mutexes. - * - * @return string - */ - protected function scheduleCache() - { - return $this->app['config']->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER')); - } - /** * Run the console application. * @@ -280,6 +275,18 @@ protected function schedule(Schedule $schedule) // } + /** + * Resolve a console schedule instance. + * + * @return \Illuminate\Console\Scheduling\Schedule + */ + public function resolveConsoleSchedule() + { + return tap(new Schedule($this->scheduleTimezone()), function ($schedule) { + $this->schedule($schedule->useCache($this->scheduleCache())); + }); + } + /** * Get the timezone that should be used by default for scheduled events. * @@ -292,6 +299,18 @@ protected function scheduleTimezone() return $config->get('app.schedule_timezone', $config->get('app.timezone')); } + /** + * Get the name of the cache store that should manage scheduling mutexes. + * + * @return string|null + */ + protected function scheduleCache() + { + return $this->app['config']->get('cache.schedule_store', Env::get('SCHEDULE_CACHE_DRIVER', function () { + return Env::get('SCHEDULE_CACHE_STORE'); + })); + } + /** * Register the commands for the application. * @@ -338,6 +357,10 @@ protected function load($paths) return; } + $this->loadedPaths = array_values( + array_unique(array_merge($this->loadedPaths, $paths)) + ); + $namespace = $this->app->getNamespace(); foreach (Finder::create()->in($paths)->files() as $file) { @@ -452,10 +475,32 @@ public function bootstrap() if (! $this->commandsLoaded) { $this->commands(); + if ($this->shouldDiscoverCommands()) { + $this->discoverCommands(); + } + $this->commandsLoaded = true; } } + /** + * Discover the commands that should be automatically loaded. + * + * @return void + */ + protected function discoverCommands() + { + foreach ($this->commandPaths as $path) { + $this->load($path); + } + + foreach ($this->commandRoutePaths as $path) { + if (file_exists($path)) { + require $path; + } + } + } + /** * Bootstrap the application without booting service providers. * @@ -470,6 +515,16 @@ public function bootstrapWithoutBootingProviders() ); } + /** + * Determine if the kernel should discover commands. + * + * @return bool + */ + protected function shouldDiscoverCommands() + { + return get_class($this) === __CLASS__; + } + /** * Get the Artisan application instance. * @@ -502,6 +557,45 @@ public function setArtisan($artisan) $this->artisan = $artisan; } + /** + * Set the Artisan commands provided by the application. + * + * @param array $commands + * @return $this + */ + public function addCommands(array $commands) + { + $this->commands = array_values(array_unique(array_merge($this->commands, $commands))); + + return $this; + } + + /** + * Set the paths that should have their Artisan commands automatically discovered. + * + * @param array $paths + * @return $this + */ + public function addCommandPaths(array $paths) + { + $this->commandPaths = array_values(array_unique(array_merge($this->commandPaths, $paths))); + + return $this; + } + + /** + * Set the paths that should have their Artisan "routes" automatically discovered. + * + * @param array $paths + * @return $this + */ + public function addCommandRoutePaths(array $paths) + { + $this->commandRoutePaths = array_values(array_unique(array_merge($this->commandRoutePaths, $paths))); + + return $this; + } + /** * Get the bootstrap classes for the application. * diff --git a/src/Illuminate/Foundation/Console/ListenerMakeCommand.php b/src/Illuminate/Foundation/Console/ListenerMakeCommand.php index d76b4b15..31bef173 100644 --- a/src/Illuminate/Foundation/Console/ListenerMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ListenerMakeCommand.php @@ -65,6 +65,19 @@ protected function buildClass($name) ); } + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$stub; + } + /** * Get the stub file for the generator. * @@ -74,13 +87,13 @@ protected function getStub() { if ($this->option('queued')) { return $this->option('event') - ? __DIR__.'/stubs/listener-queued.stub' - : __DIR__.'/stubs/listener-queued-duck.stub'; + ? $this->resolveStubPath('/stubs/listener.typed.queued.stub') + : $this->resolveStubPath('/stubs/listener.queued.stub'); } return $this->option('event') - ? __DIR__.'/stubs/listener.stub' - : __DIR__.'/stubs/listener-duck.stub'; + ? $this->resolveStubPath('/stubs/listener.typed.stub') + : $this->resolveStubPath('/stubs/listener.stub'); } /** diff --git a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php index 7e932d79..c75ab3a9 100644 --- a/src/Illuminate/Foundation/Console/OptimizeClearCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeClearCommand.php @@ -32,12 +32,12 @@ public function handle() $this->components->info('Clearing cached bootstrap files.'); collect([ - 'events' => fn () => $this->callSilent('event:clear') == 0, - 'views' => fn () => $this->callSilent('view:clear') == 0, 'cache' => fn () => $this->callSilent('cache:clear') == 0, - 'route' => fn () => $this->callSilent('route:clear') == 0, - 'config' => fn () => $this->callSilent('config:clear') == 0, 'compiled' => fn () => $this->callSilent('clear-compiled') == 0, + 'config' => fn () => $this->callSilent('config:clear') == 0, + 'events' => fn () => $this->callSilent('event:clear') == 0, + 'route' => fn () => $this->callSilent('route:clear') == 0, + 'views' => fn () => $this->callSilent('view:clear') == 0, ])->each(fn ($task, $description) => $this->components->task($description, $task)); $this->newLine(); diff --git a/src/Illuminate/Foundation/Console/OptimizeCommand.php b/src/Illuminate/Foundation/Console/OptimizeCommand.php index 10f26462..0bcf3e97 100644 --- a/src/Illuminate/Foundation/Console/OptimizeCommand.php +++ b/src/Illuminate/Foundation/Console/OptimizeCommand.php @@ -20,7 +20,7 @@ class OptimizeCommand extends Command * * @var string */ - protected $description = 'Cache the framework bootstrap files'; + protected $description = 'Cache framework bootstrap, configuration, and metadata to increase performance'; /** * Execute the console command. @@ -29,11 +29,13 @@ class OptimizeCommand extends Command */ public function handle() { - $this->components->info('Caching the framework bootstrap files'); + $this->components->info('Caching framework bootstrap, configuration, and metadata.'); collect([ 'config' => fn () => $this->callSilent('config:cache') == 0, + 'events' => fn () => $this->callSilent('event:cache') == 0, 'routes' => fn () => $this->callSilent('route:cache') == 0, + 'views' => fn () => $this->callSilent('view:cache') == 0, ])->each(fn ($task, $description) => $this->components->task($description, $task)); $this->newLine(); diff --git a/src/Illuminate/Foundation/Console/ProviderMakeCommand.php b/src/Illuminate/Foundation/Console/ProviderMakeCommand.php index 54d7e4c0..d955c825 100644 --- a/src/Illuminate/Foundation/Console/ProviderMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ProviderMakeCommand.php @@ -3,6 +3,7 @@ namespace Illuminate\Foundation\Console; use Illuminate\Console\GeneratorCommand; +use Illuminate\Support\ServiceProvider; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputOption; @@ -30,6 +31,29 @@ class ProviderMakeCommand extends GeneratorCommand */ protected $type = 'Provider'; + /** + * Execute the console command. + * + * @return bool|null + * + * @throws \Illuminate\Contracts\Filesystem\FileNotFoundException + */ + public function handle() + { + $result = parent::handle(); + + if ($result === false) { + return $result; + } + + ServiceProvider::addProviderToBootstrapFile( + $this->qualifyClass($this->getNameInput()), + $this->laravel->getBootstrapProvidersPath(), + ); + + return $result; + } + /** * Get the stub file for the generator. * diff --git a/src/Illuminate/Foundation/Console/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index 194c80fa..a73bf52e 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/StubPublishCommand.php b/src/Illuminate/Foundation/Console/StubPublishCommand.php index e2a293d4..3ee37b48 100644 --- a/src/Illuminate/Foundation/Console/StubPublishCommand.php +++ b/src/Illuminate/Foundation/Console/StubPublishCommand.php @@ -40,10 +40,18 @@ 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', + __DIR__.'/stubs/listener.typed.queued.stub' => 'listener.typed.queued.stub', + __DIR__.'/stubs/listener.queued.stub' => 'listener.queued.stub', + __DIR__.'/stubs/listener.typed.stub' => 'listener.typed.stub', + __DIR__.'/stubs/listener.stub' => 'listener.stub', __DIR__.'/stubs/mail.stub' => 'mail.stub', __DIR__.'/stubs/markdown-mail.stub' => 'markdown-mail.stub', __DIR__.'/stubs/markdown-notification.stub' => 'markdown-notification.stub', @@ -62,6 +70,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 7b65cf99..85440589 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 00000000..a4542018 --- /dev/null +++ b/src/Illuminate/Foundation/Console/TraitMakeCommand.php @@ -0,0 +1,78 @@ +resolveStubPath('/stubs/trait.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$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 [ + ['force', 'f', InputOption::VALUE_NONE, 'Create the trait even if the trait already exists'], + ]; + } +} diff --git a/src/Illuminate/Foundation/Console/VendorPublishCommand.php b/src/Illuminate/Foundation/Console/VendorPublishCommand.php index f755c3d0..78dd9b68 100644 --- a/src/Illuminate/Foundation/Console/VendorPublishCommand.php +++ b/src/Illuminate/Foundation/Console/VendorPublishCommand.php @@ -42,6 +42,13 @@ class VendorPublishCommand extends Command */ protected $tags = []; + /** + * The time the command started. + * + * @var \Illuminate\Support\Carbon|null + */ + protected $publishedAt; + /** * The console command signature. * @@ -61,6 +68,13 @@ class VendorPublishCommand extends Command */ protected $description = 'Publish any publishable assets from vendor packages'; + /** + * Indicates if migration dates should be updated while publishing. + * + * @var bool + */ + protected static $updateMigrationDates = true; + /** * Create a new command instance. * @@ -81,6 +95,8 @@ public function __construct(Filesystem $files) */ public function handle() { + $this->publishedAt = now(); + $this->determineWhatShouldBePublished(); foreach ($this->tags ?: [null] as $tag) { @@ -243,6 +259,8 @@ protected function publishFile($from, $to) { if ((! $this->option('existing') && (! $this->files->exists($to) || $this->option('force'))) || ($this->option('existing') && $this->files->exists($to))) { + $to = $this->ensureMigrationNameIsUpToDate($from, $to); + $this->createParentDirectory(dirname($to)); $this->files->copy($from, $to); @@ -274,7 +292,7 @@ protected function publishDirectory($from, $to) { $visibility = PortableVisibilityConverter::fromArray([], Visibility::PUBLIC); - $this->moveManagedFiles(new MountManager([ + $this->moveManagedFiles($from, new MountManager([ 'from' => new Flysystem(new LocalAdapter($from)), 'to' => new Flysystem(new LocalAdapter($to, $visibility)), ])); @@ -285,10 +303,11 @@ protected function publishDirectory($from, $to) /** * Move all the files in the given MountManager. * + * @param string $from * @param \League\Flysystem\MountManager $manager * @return void */ - protected function moveManagedFiles($manager) + protected function moveManagedFiles($from, $manager) { foreach ($manager->listContents('from://', true) as $file) { $path = Str::after($file['path'], 'from://'); @@ -300,6 +319,8 @@ protected function moveManagedFiles($manager) || ($this->option('existing') && $manager->fileExists('to://'.$path)) ) ) { + $path = $this->ensureMigrationNameIsUpToDate($from, $path); + $manager->write('to://'.$path, $manager->read($file['path'])); } } @@ -318,6 +339,38 @@ protected function createParentDirectory($directory) } } + /** + * Ensure the given migration name is up-to-date. + * + * @param string $from + * @param string $to + * @return string + */ + protected function ensureMigrationNameIsUpToDate($from, $to) + { + if (static::$updateMigrationDates === false) { + return $to; + } + + $from = realpath($from); + + foreach (ServiceProvider::publishableMigrationPaths() as $path) { + $path = realpath($path); + + if ($from === $path && preg_match('/\d{4}_(\d{2})_(\d{2})_(\d{6})_/', $to)) { + $this->publishedAt->addSecond(); + + return preg_replace( + '/\d{4}_(\d{2})_(\d{2})_(\d{6})_/', + $this->publishedAt->format('Y_m_d_His').'_', + $to, + ); + } + } + + return $to; + } + /** * Write a status message to the console. * @@ -339,4 +392,14 @@ protected function status($from, $to, $type) $to, )); } + + /** + * Intruct the command to not update the dates on migrations when publishing. + * + * @return void + */ + public static function dontUpdateMigrationDates() + { + static::$updateMigrationDates = false; + } } diff --git a/src/Illuminate/Foundation/Console/ViewMakeCommand.php b/src/Illuminate/Foundation/Console/ViewMakeCommand.php index 49899312..4708f7d9 100644 --- a/src/Illuminate/Foundation/Console/ViewMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ViewMakeCommand.php @@ -130,7 +130,7 @@ protected function getTestPath() */ protected function handleTestCreation($path): bool { - if (! $this->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 new file mode 100644 index 00000000..ccc387f2 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/api-routes.stub @@ -0,0 +1,8 @@ +user(); +})->middleware('auth:sanctum'); diff --git a/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub b/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub new file mode 100644 index 00000000..df2ad287 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/broadcasting-routes.stub @@ -0,0 +1,7 @@ +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 00000000..c55610cf --- /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 00000000..e4098477 --- /dev/null +++ b/src/Illuminate/Foundation/Console/stubs/trait.stub @@ -0,0 +1,8 @@ +ignore($exceptions); + } + + /** + * Indicate that the given exception type should not be reported. + * + * @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; + } + + /** + * Indicate that the given attributes should never be flashed to the session on validation errors. + * + * @param array|string $attributes + * @return $this + */ + public function dontFlash(array|string $attributes) + { + $this->dontFlash = array_values(array_unique( + array_merge($this->dontFlash, Arr::wrap($attributes)) + )); return $this; } @@ -360,7 +419,7 @@ protected function shouldntReport(Throwable $e) with($throttle->key ?: 'illuminate:foundation:exceptions:'.$e::class, fn ($key) => $this->hashThrottleKeys ? md5($key) : $key), $throttle->maxAttempts, fn () => true, - 60 * $throttle->decayMinutes + $throttle->decaySeconds ); }), rescue: false, report: false); } @@ -373,22 +432,53 @@ protected function shouldntReport(Throwable $e) */ protected function throttle(Throwable $e) { + foreach ($this->throttleCallbacks as $throttleCallback) { + foreach ($this->firstClosureParameterTypes($throttleCallback) as $type) { + if (is_a($e, $type)) { + $response = $throttleCallback($e); + + if (! is_null($response)) { + return $response; + } + } + } + } + return Limit::none(); } + /** + * Specify the callback that should be used to throttle reportable exceptions. + * + * @param callable $throttleUsing + * @return $this + */ + public function throttleUsing(callable $throttleUsing) + { + if (! $throttleUsing instanceof Closure) { + $throttleUsing = Closure::fromCallable($throttleUsing); + } + + $this->throttleCallbacks[] = $throttleUsing; + + return $this; + } + /** * 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; } @@ -416,11 +506,17 @@ protected function buildExceptionContext(Throwable $e) */ protected function exceptionContext(Throwable $e) { + $context = []; + if (method_exists($e, 'context')) { - return $e->context(); + $context = $e->context(); } - return []; + foreach ($this->contextCallbacks as $callback) { + $context = array_merge($context, $callback($e, $context)); + } + + return $context; } /** @@ -439,6 +535,19 @@ protected function context() } } + /** + * Register a closure that should be used to build exception context data. + * + * @param \Closure $contextCallback + * @return $this + */ + public function buildContextUsing(Closure $contextCallback) + { + $this->contextCallbacks[] = $contextCallback; + + return $this; + } + /** * Render an exception into an HTTP response. * @@ -453,25 +562,57 @@ public function render($request, Throwable $e) $e = $this->mapException($e); if (method_exists($e, 'render') && $response = $e->render($request)) { - return Router::toResponse($request, $response); + return $this->finalizeRenderedResponse( + $request, + Router::toResponse($request, $response), + $e + ); } if ($e instanceof Responsable) { - return $e->toResponse($request); + return $this->finalizeRenderedResponse($request, $e->toResponse($request), $e); } $e = $this->prepareException($e); if ($response = $this->renderViaCallbacks($request, $e)) { - return $response; + return $this->finalizeRenderedResponse($request, $response, $e); } - return match (true) { + return $this->finalizeRenderedResponse($request, match (true) { $e instanceof HttpResponseException => $e->getResponse(), $e instanceof AuthenticationException => $this->unauthenticated($request, $e), $e instanceof ValidationException => $this->convertValidationExceptionToResponse($e, $request), default => $this->renderExceptionResponse($request, $e), - }; + }, $e); + } + + /** + * Prepare the final, rendered response to be returned to the browser. + * + * @param \Illuminate\Http\Request $request + * @param \Symfony\Component\HttpFoundation\Response $response + * @param \Throwable $e + * @return \Symfony\Component\HttpFoundation\Response + */ + protected function finalizeRenderedResponse($request, $response, Throwable $e) + { + return $this->finalizeResponseCallback + ? call_user_func($this->finalizeResponseCallback, $response, $e, $request) + : $response; + } + + /** + * Prepare the final, rendered response for an exception using the given callback. + * + * @param callable $callback + * @return $this + */ + public function respondUsing($callback) + { + $this->finalizeResponseCallback = $callback; + + return $this; } /** @@ -490,7 +631,7 @@ protected function prepareException(Throwable $e) ), $e instanceof AuthorizationException && ! $e->hasStatus() => new AccessDeniedHttpException($e->getMessage(), $e), $e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e), - $e instanceof SuspiciousOperationException => new NotFoundHttpException('Bad hostname provided.', $e), + $e instanceof RequestExceptionInterface => new BadRequestHttpException('Bad request.', $e), $e instanceof RecordsNotFoundException => new NotFoundHttpException('Not found.', $e), default => $e, }; @@ -567,7 +708,7 @@ protected function unauthenticated($request, AuthenticationException $exception) { return $this->shouldReturnJson($request, $exception) ? response()->json(['message' => $exception->getMessage()], 401) - : redirect()->guest($exception->redirectTo() ?? route('login')); + : redirect()->guest($exception->redirectTo($request) ?? route('login')); } /** @@ -626,7 +767,22 @@ protected function invalidJson($request, ValidationException $exception) */ protected function shouldReturnJson($request, Throwable $e) { - return $request->expectsJson(); + return $this->shouldRenderJsonWhenCallback + ? call_user_func($this->shouldRenderJsonWhenCallback, $request, $e) + : $request->expectsJson(); + } + + /** + * Register the callable that determines if the exception handler response should be JSON. + * + * @param callable(\Illuminate\Http\Request $request, \Throwable): bool $callback + * @return $this + */ + public function shouldRenderJsonWhen($callback) + { + $this->shouldRenderJsonWhenCallback = $callback; + + return $this; } /** diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 93e7d200..4824c27c 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -115,9 +115,11 @@ protected function getValidatorInstance() */ protected function createDefaultValidator(ValidationFactory $factory) { + $rules = $this->validationRules(); + $validator = $factory->make( $this->validationData(), - $this->validationRules(), + $rules, $this->messages(), $this->attributes(), )->stopOnFirstFailure($this->stopOnFirstFailure); diff --git a/src/Illuminate/Foundation/Http/Kernel.php b/src/Illuminate/Foundation/Http/Kernel.php index 51ad7929..79d5a6d5 100644 --- a/src/Illuminate/Foundation/Http/Kernel.php +++ b/src/Illuminate/Foundation/Http/Kernel.php @@ -509,6 +509,31 @@ protected function renderException($request, Throwable $e) return $this->app[ExceptionHandler::class]->render($request, $e); } + /** + * Get the application's global middleware. + * + * @return array + */ + public function getGlobalMiddleware() + { + return $this->middleware; + } + + /** + * Set the application's global middleware. + * + * @param array $middleware + * @return $this + */ + public function setGlobalMiddleware(array $middleware) + { + $this->middleware = $middleware; + + $this->syncMiddlewareToRouter(); + + return $this; + } + /** * Get the application's route middleware groups. * @@ -519,6 +544,21 @@ public function getMiddlewareGroups() return $this->middlewareGroups; } + /** + * Set the application's middleware groups. + * + * @param array $groups + * @return $this + */ + public function setMiddlewareGroups(array $groups) + { + $this->middlewareGroups = $groups; + + $this->syncMiddlewareToRouter(); + + return $this; + } + /** * Get the application's route middleware aliases. * @@ -541,6 +581,36 @@ public function getMiddlewareAliases() return array_merge($this->routeMiddleware, $this->middlewareAliases); } + /** + * Set the application's route middleware aliases. + * + * @param array $aliases + * @return $this + */ + public function setMiddlewareAliases(array $aliases) + { + $this->middlewareAliases = $aliases; + + $this->syncMiddlewareToRouter(); + + 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 00000000..941b2fec --- /dev/null +++ b/src/Illuminate/Foundation/Http/Middleware/Concerns/ExcludesPaths.php @@ -0,0 +1,37 @@ +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 a25e4c4a..6adb87d0 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,12 +22,19 @@ class PreventRequestsDuringMaintenance protected $app; /** - * The URIs that should be accessible while maintenance mode is enabled. + * The URIs that should be excluded. * * @var array */ protected $except = []; + /** + * The URIs that should be accessible during maintenance. + * + * @var array + */ + protected static $neverPrevent = []; + /** * Create a new middleware instance. * @@ -116,27 +127,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 +158,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 3b7c6c4f..f5ad195b 100644 --- a/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php +++ b/src/Illuminate/Foundation/Http/Middleware/TrimStrings.php @@ -3,24 +3,34 @@ namespace Illuminate\Foundation\Http\Middleware; use Closure; +use Illuminate\Support\Arr; class TrimStrings extends TransformsRequest { /** - * All of the registered skip callbacks. + * The attributes that should not be trimmed. + * + * @var array + */ + protected $except = [ + 'current_password', + 'password', + 'password_confirmation', + ]; + + /** + * The globally ignored attributes that should not be trimmed. * * @var array */ - protected static $skipCallbacks = []; + protected static $neverTrim = []; /** - * The attributes that should not be trimmed. + * All of the registered skip callbacks. * - * @var array + * @var array */ - protected $except = [ - // - ]; + protected static $skipCallbacks = []; /** * Handle an incoming request. @@ -49,13 +59,28 @@ public function handle($request, Closure $next) */ protected function transform($key, $value) { - if (in_array($key, $this->except, true) || ! is_string($value)) { + $except = array_merge($this->except, static::$neverTrim); + + if (in_array($key, $except, true) || ! is_string($value)) { return $value; } return preg_replace('~^[\s\x{FEFF}\x{200B}]+|[\s\x{FEFF}\x{200B}]+$~u', '', $value) ?? trim($value); } + /** + * Indicate that the given attributes should never be trimmed. + * + * @param array|string $attributes + * @return void + */ + public static function except($attributes) + { + static::$neverTrim = array_values(array_unique( + array_merge(static::$neverTrim, Arr::wrap($attributes)) + )); + } + /** * Register a callback that instructs the middleware to be skipped. * @@ -74,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/ValidateCsrfToken.php b/src/Illuminate/Foundation/Http/Middleware/ValidateCsrfToken.php new file mode 100644 index 00000000..f49d6141 --- /dev/null +++ b/src/Illuminate/Foundation/Http/Middleware/ValidateCsrfToken.php @@ -0,0 +1,11 @@ +getPostMaxSize(); - - if ($max > 0 && $request->server('CONTENT_LENGTH') > $max) { - throw new PostTooLargeException; - } - - return $next($request); - } - - /** - * Determine the server 'post_max_size' as bytes. - * - * @return int - */ - protected function getPostMaxSize() - { - if (is_numeric($postMaxSize = ini_get('post_max_size'))) { - return (int) $postMaxSize; - } - - $metric = strtoupper(substr($postMaxSize, -1)); - $postMaxSize = (int) $postMaxSize; - - return match ($metric) { - 'K' => $postMaxSize * 1024, - 'M' => $postMaxSize * 1048576, - 'G' => $postMaxSize * 1073741824, - default => $postMaxSize, - }; - } + // } diff --git a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php index 25413940..5edc53d3 100644 --- a/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php +++ b/src/Illuminate/Foundation/Http/Middleware/VerifyCsrfToken.php @@ -9,13 +9,16 @@ 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; use Symfony\Component\HttpFoundation\Cookie; class VerifyCsrfToken { - use InteractsWithTime; + use InteractsWithTime, + ExcludesPaths; /** * The application instance. @@ -32,12 +35,19 @@ class VerifyCsrfToken protected $encrypter; /** - * The URIs that should be excluded from CSRF verification. + * The URIs that should be excluded. * * @var array */ protected $except = []; + /** + * The globally ignored URIs that should be excluded from CSRF verification. + * + * @var array + */ + protected static $neverVerify = []; + /** * Indicates whether the XSRF-TOKEN cookie should be set on the response. * @@ -107,24 +117,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 ($this->except 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); } /** @@ -216,6 +215,19 @@ protected function newCookie($request, $config) ); } + /** + * Indicate that the given URIs should be excluded from CSRF verification. + * + * @param array|string $uris + * @return void + */ + public static function except($uris) + { + static::$neverVerify = array_values(array_unique( + array_merge(static::$neverVerify, Arr::wrap($uris)) + )); + } + /** * Determine if the cookie contents should be serialized. * @@ -225,4 +237,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 482ccc35..4ec82896 100755 --- a/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/ArtisanServiceProvider.php @@ -28,17 +28,22 @@ use Illuminate\Database\Console\TableCommand as DatabaseTableCommand; use Illuminate\Database\Console\WipeCommand; use Illuminate\Foundation\Console\AboutCommand; +use Illuminate\Foundation\Console\ApiInstallCommand; +use Illuminate\Foundation\Console\BroadcastingInstallCommand; 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; use Illuminate\Foundation\Console\ConfigClearCommand; +use Illuminate\Foundation\Console\ConfigPublishCommand; use Illuminate\Foundation\Console\ConfigShowCommand; 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; @@ -48,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,6 +79,7 @@ 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; @@ -171,18 +178,24 @@ class ArtisanServiceProvider extends ServiceProvider implements DeferrableProvid * @var array */ protected $devCommands = [ + 'ApiInstall' => ApiInstallCommand::class, + 'BroadcastingInstall' => BroadcastingInstallCommand::class, 'CacheTable' => CacheTableCommand::class, '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, @@ -206,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, ]; @@ -294,7 +308,7 @@ protected function registerCacheForgetCommand() protected function registerCacheTableCommand() { $this->app->singleton(CacheTableCommand::class, function ($app) { - return new CacheTableCommand($app['files'], $app['composer']); + return new CacheTableCommand($app['files']); }); } @@ -322,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. * @@ -358,6 +384,18 @@ protected function registerConfigClearCommand() }); } + /** + * Register the command. + * + * @return void + */ + protected function registerConfigPublishCommand() + { + $this->app->singleton(ConfigPublishCommand::class, function ($app) { + return new ConfigPublishCommand; + }); + } + /** * Register the command. * @@ -382,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. * @@ -430,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. * @@ -510,7 +572,7 @@ protected function registerNotificationMakeCommand() protected function registerNotificationTableCommand() { $this->app->singleton(NotificationTableCommand::class, function ($app) { - return new NotificationTableCommand($app['files'], $app['composer']); + return new NotificationTableCommand($app['files']); }); } @@ -640,7 +702,7 @@ protected function registerQueueWorkCommand() protected function registerQueueFailedTableCommand() { $this->app->singleton(FailedTableCommand::class, function ($app) { - return new FailedTableCommand($app['files'], $app['composer']); + return new FailedTableCommand($app['files']); }); } @@ -652,7 +714,7 @@ protected function registerQueueFailedTableCommand() protected function registerQueueTableCommand() { $this->app->singleton(TableCommand::class, function ($app) { - return new TableCommand($app['files'], $app['composer']); + return new TableCommand($app['files']); }); } @@ -664,7 +726,7 @@ protected function registerQueueTableCommand() protected function registerQueueBatchesTableCommand() { $this->app->singleton(BatchesTableCommand::class, function ($app) { - return new BatchesTableCommand($app['files'], $app['composer']); + return new BatchesTableCommand($app['files']); }); } @@ -724,7 +786,7 @@ protected function registerScopeMakeCommand() protected function registerSeederMakeCommand() { $this->app->singleton(SeederMakeCommand::class, function ($app) { - return new SeederMakeCommand($app['files'], $app['composer']); + return new SeederMakeCommand($app['files']); }); } @@ -736,7 +798,7 @@ protected function registerSeederMakeCommand() protected function registerSessionTableCommand() { $this->app->singleton(SessionTableCommand::class, function ($app) { - return new SessionTableCommand($app['files'], $app['composer']); + return new SessionTableCommand($app['files']); }); } @@ -800,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 4c11ce00..341ae00c 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -2,6 +2,8 @@ namespace Illuminate\Foundation\Providers; +use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Contracts\Console\Kernel as ConsoleKernel; use Illuminate\Contracts\Container\Container; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Foundation\MaintenanceMode as MaintenanceModeContract; @@ -69,6 +71,7 @@ public function register() { parent::register(); + $this->registerConsoleSchedule(); $this->registerDumper(); $this->registerRequestValidation(); $this->registerRequestSignatureValidation(); @@ -76,6 +79,18 @@ public function register() $this->registerMaintenanceModeManager(); } + /** + * Register the console schedule implementation. + * + * @return void + */ + public function registerConsoleSchedule() + { + $this->app->singleton(Schedule::class, function ($app) { + return $app->make(ConsoleKernel::class)->resolveConsoleSchedule(); + }); + } + /** * Register a var dumper (with source) to debug variables. * @@ -153,6 +168,10 @@ public function registerRequestSignatureValidation() Request::macro('hasValidSignatureWhileIgnoring', function ($ignoreQuery = [], $absolute = true) { return URL::hasValidSignature($this, $absolute, $ignoreQuery); }); + + Request::macro('hasValidRelativeSignatureWhileIgnoring', function ($ignoreQuery = []) { + return URL::hasValidSignature($this, $absolute = false, $ignoreQuery); + }); } /** diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index d966c5fd..69f4e63f 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -2,7 +2,10 @@ namespace Illuminate\Foundation\Support\Providers; +use Illuminate\Auth\Events\Registered; +use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Events\DiscoverEvents; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; @@ -29,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. * @@ -53,6 +63,10 @@ public function register() $model::observe($observers); } }); + + $this->booted(function () { + $this->configureEmailVerification(); + }); } /** @@ -113,7 +127,7 @@ protected function discoveredEvents() */ public function shouldDiscoverEvents() { - return false; + return get_class($this) === __CLASS__; } /** @@ -142,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. * @@ -156,4 +181,17 @@ protected function eventDiscoveryBasePath() { return base_path(); } + + /** + * Configure the proper event listeners for email verification. + * + * @return void + */ + protected function configureEmailVerification() + { + if (! isset($this->listen[Registered::class]) || + ! in_array(SendEmailVerificationNotification::class, Arr::wrap($this->listen[Registered::class]))) { + Event::listen(Registered::class, SendEmailVerificationNotification::class); + } + } } diff --git a/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php index c8679e51..72ba1cc1 100644 --- a/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/RouteServiceProvider.php @@ -29,6 +29,13 @@ class RouteServiceProvider extends ServiceProvider */ protected $loadRoutesUsing; + /** + * The global callback that should be used to load the application's routes. + * + * @var \Closure|null + */ + protected static $alwaysLoadRoutesUsing; + /** * Register any application services. * @@ -75,6 +82,17 @@ protected function routes(Closure $routesCallback) return $this; } + /** + * Register the callback that will be used to load the application's routes. + * + * @param \Closure|null $routesCallback + * @return void + */ + public static function loadRoutesUsing(?Closure $routesCallback) + { + self::$alwaysLoadRoutesUsing = $routesCallback; + } + /** * Set the root controller namespace for the application. * @@ -116,6 +134,10 @@ protected function loadCachedRoutes() */ protected function loadRoutes() { + 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')) { diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php index c5d4e1aa..9c5060b3 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithConsole.php @@ -15,6 +15,13 @@ trait InteractsWithConsole */ public $mockConsoleOutput = true; + /** + * Indicates if the command is expected to output anything. + * + * @var bool|null + */ + public $expectsOutput; + /** * All of the expected output lines. * diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php new file mode 100644 index 00000000..3d210cd7 --- /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 6717b089..a5468e37 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php +++ b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php @@ -592,7 +592,7 @@ public function call($method, $uri, $parameters = [], $cookies = [], $files = [] $response = $this->followRedirects($response); } - return static::$latestResponse = $this->createTestResponse($response); + return static::$latestResponse = $this->createTestResponse($response, $request); } /** @@ -725,11 +725,12 @@ protected function createTestRequest($symfonyRequest) * Create the test response instance from the given response. * * @param \Illuminate\Http\Response $response + * @param \Illuminate\Http\Request $request * @return \Illuminate\Testing\TestResponse */ - protected function createTestResponse($response) + protected function createTestResponse($response, $request) { - return tap(TestResponse::fromBaseResponse($response), function ($response) { + return tap(TestResponse::fromBaseResponse($response, $request), function ($response) { $response->withExceptions( $this->app->bound(LoggedExceptionCollection::class) ? $this->app->make(LoggedExceptionCollection::class) diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php index cea2d8d0..3f43181e 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), @@ -130,9 +130,11 @@ protected function connectionsToTruncate(): array */ protected function exceptTables(?string $connectionName): array { - if (property_exists($this, 'exceptTables')) { - $migrationsTable = $this->app['config']->get('database.migrations'); + $migrations = $this->app['config']->get('database.migrations'); + + $migrationsTable = is_array($migrations) ? ($migrations['table'] ?? null) : $migrations; + if (property_exists($this, 'exceptTables')) { if (array_is_list($this->exceptTables ?? [])) { return array_merge( $this->exceptTables ?? [], @@ -146,7 +148,7 @@ protected function exceptTables(?string $connectionName): array ); } - return [$this->app['config']->get('database.migrations')]; + return [$migrationsTable]; } /** diff --git a/src/Illuminate/Foundation/Testing/RefreshDatabase.php b/src/Illuminate/Foundation/Testing/RefreshDatabase.php index 0f916ac5..4c4e084a 100644 --- a/src/Illuminate/Foundation/Testing/RefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/RefreshDatabase.php @@ -18,9 +18,11 @@ public function refreshDatabase() { $this->beforeRefreshingDatabase(); - $this->usingInMemoryDatabase() - ? $this->refreshInMemoryDatabase() - : $this->refreshTestDatabase(); + if ($this->usingInMemoryDatabase()) { + $this->restoreInMemoryDatabase(); + } + + $this->refreshTestDatabase(); $this->afterRefreshingDatabase(); } @@ -38,28 +40,19 @@ protected function usingInMemoryDatabase() } /** - * Refresh the in-memory database. + * Restore the in-memory database between tests. * * @return void */ - protected function refreshInMemoryDatabase() + protected function restoreInMemoryDatabase() { - $this->artisan('migrate', $this->migrateUsing()); - - $this->app[Kernel::class]->setArtisan(null); - } + $database = $this->app->make('db'); - /** - * The parameters that should be used when running "migrate". - * - * @return array - */ - protected function migrateUsing() - { - return [ - '--seed' => $this->shouldSeed(), - '--seeder' => $this->seeder(), - ]; + foreach ($this->connectionsToTransact() as $name) { + if (isset(RefreshDatabaseState::$inMemoryConnections[$name])) { + $database->connection($name)->setPdo(RefreshDatabaseState::$inMemoryConnections[$name]); + } + } } /** @@ -93,7 +86,13 @@ public function beginDatabaseTransaction() foreach ($this->connectionsToTransact() as $name) { $connection = $database->connection($name); + $connection->setTransactionManager($transactionsManager); + + if ($this->usingInMemoryDatabase()) { + RefreshDatabaseState::$inMemoryConnections[$name] ??= $connection->getPdo(); + } + $dispatcher = $connection->getEventDispatcher(); $connection->unsetEventDispatcher(); diff --git a/src/Illuminate/Foundation/Testing/RefreshDatabaseState.php b/src/Illuminate/Foundation/Testing/RefreshDatabaseState.php index a42d3d08..e64c34a2 100644 --- a/src/Illuminate/Foundation/Testing/RefreshDatabaseState.php +++ b/src/Illuminate/Foundation/Testing/RefreshDatabaseState.php @@ -4,6 +4,13 @@ class RefreshDatabaseState { + /** + * The current SQLite in-memory database connections. + * + * @var array + */ + public static $inMemoryConnections = []; + /** * Indicates if the test database has been migrated. * diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 152b5c41..94ec4c71 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -2,22 +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\Console\AboutCommand; -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; @@ -32,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. @@ -87,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(); } /** @@ -116,74 +57,18 @@ 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[WithoutEvents::class])) { - $this->disableEventsForAllTests(); - } - - 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} */ - protected function runTest(): mixed + protected function transformException(Throwable $error): Throwable { - $result = null; + $response = static::$latestResponse ?? null; - try { - $result = parent::runTest(); - } catch (Throwable $e) { - if (! is_null(static::$latestResponse)) { - static::$latestResponse->transformNotSuccessfulException($e); - } - - throw $e; + if (! is_null($response)) { + $response->transformNotSuccessfulException($error); } - return $result; + return $error; } /** @@ -195,68 +80,7 @@ protected function runTest(): mixed */ 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; - - AboutCommand::flushState(); - 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(); } /** @@ -268,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/Testing/WithoutEvents.php b/src/Illuminate/Foundation/Testing/WithoutEvents.php deleted file mode 100644 index fa5df3ce..00000000 --- a/src/Illuminate/Foundation/Testing/WithoutEvents.php +++ /dev/null @@ -1,22 +0,0 @@ -withoutEvents(); - } else { - throw new Exception('Unable to disable events. ApplicationTrait not used.'); - } - } -} 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 00000000..cb4689a8 --- /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/Roots/Acorn/Application.php b/src/Roots/Acorn/Application.php index cc5f8ef6..ede996ff 100644 --- a/src/Roots/Acorn/Application.php +++ b/src/Roots/Acorn/Application.php @@ -9,6 +9,9 @@ use Illuminate\Foundation\ProviderRepository; use Illuminate\Support\Collection; use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Str; +use Roots\Acorn\Application\Concerns\Bootable; +use Roots\Acorn\Configuration\ApplicationBuilder; use Roots\Acorn\Exceptions\SkipProviderException; use Roots\Acorn\Filesystem\Filesystem; use RuntimeException; @@ -19,6 +22,8 @@ */ class Application extends FoundationApplication { + use Bootable; + /** * The Acorn framework version. * @@ -27,34 +32,67 @@ class Application extends FoundationApplication public const VERSION = '5.x-dev'; /** - * The custom resources path defined by the developer. + * The custom resource path defined by the developer. * * @var string */ protected $resourcePath; /** - * Create a new Illuminate application instance. + * Create a new Application instance. * * @param string|null $basePath - * @param array|null $paths * @return void */ - public function __construct($basePath = null, $paths = null) + public function __construct($basePath = null) { if ($basePath) { $this->basePath = rtrim($basePath, '\/'); } - if ($paths) { - $this->usePaths((array) $paths); - } + $this->useEnvironmentPath($this->environmentPath()); $this->registerGlobalHelpers(); parent::__construct($basePath); } + /** + * Begin configuring a new Laravel application instance. + * + * @return \Roots\Acorn\Configuration\ApplicationBuilder + */ + public static function configure(?string $basePath = null) + { + $basePath = match (true) { + is_string($basePath) => $basePath, + default => ApplicationBuilder::inferBasePath(), + }; + + return (new ApplicationBuilder(new static($basePath))) + ->withPaths() + ->withKernels() + ->withEvents() + ->withCommands() + ->withProviders() + ->withRouting( + web: $basePath.'/routes/web.php', + api: $basePath.'/routes/api.php', + ) + ->withMiddleware() + ->withExceptions(); + } + + /** + * Get the environment file path. + */ + public function environmentPath(): string + { + return is_file($envPath = (new Filesystem)->closest($this->basePath(), '.env') ?? '') + ? dirname($envPath) + : $this->basePath(); + } + /** * Load global helper functions. * @@ -77,8 +115,8 @@ protected function registerGlobalHelpers() * - public * - resources * - storage + * - environment * - * @param array $path * @return $this */ public function usePaths(array $paths) @@ -92,6 +130,7 @@ public function usePaths(array $paths) 'database' => 'databasePath', 'resources' => 'resourcePath', 'bootstrap' => 'bootstrapPath', + 'environment' => 'environmentPath', ]; foreach ($paths as $pathType => $path) { @@ -101,7 +140,9 @@ public function usePaths(array $paths) throw new Exception("The {$pathType} path type is not supported."); } - $this->{$supportedPaths[$pathType]} = $path; + $this->{$supportedPaths[$pathType]} = Str::startsWith($path, $this->absoluteCachePathPrefixes) + ? $path + : $this->basePath($path); } $this->bindPathsInContainer(); @@ -123,13 +164,18 @@ protected function bindPathsInContainer() $this->instance('path.public', $this->publicPath()); $this->instance('path.resources', $this->resourcePath()); $this->instance('path.storage', $this->storagePath()); - $this->instance('path.bootstrap', $this->bootstrapPath()); - $this->useLangPath(value(function () { - return is_dir($directory = $this->resourcePath('lang')) + $this->useBootstrapPath(value(function () { + return is_dir($directory = $this->basePath('.laravel')) ? $directory - : $this->basePath('lang'); + : $this->bootstrapPath(); })); + + $this->useLangPath(value( + fn () => is_dir($directory = $this->resourcePath('lang')) + ? $directory + : $this->basePath('lang') + )); } /** @@ -177,9 +223,15 @@ public function useResourcePath($path) protected function registerBaseBindings() { parent::registerBaseBindings(); + $this->registerPackageManifest(); } + /** + * Register the package manifest. + * + * @return void + */ protected function registerPackageManifest() { $this->singleton(FoundationPackageManifest::class, function () { @@ -195,8 +247,9 @@ protected function registerPackageManifest() ]) ->map(fn ($path) => rtrim($files->normalizePath($path), '/')) ->unique() - ->filter(fn ($path) => @$files->isFile("{$path}/vendor/composer/installed.json") && - @$files->isFile("{$path}/composer.json") + ->filter( + fn ($path) => @$files->isFile("{$path}/vendor/composer/installed.json") && + @$files->isFile("{$path}/composer.json") )->all(); return new PackageManifest( @@ -292,9 +345,10 @@ protected function skipProvider($provider, Throwable $e): ServiceProvider $providerName = is_object($provider) ? get_class($provider) : $provider; if (! $e instanceof SkipProviderException) { + $error = get_class($e); $message = [ BindingResolutionException::class => "Skipping provider [{$providerName}] because it requires a dependency that cannot be found.", - ][$error = get_class($e)] ?? "Skipping provider [{$providerName}] because it encountered an error [{$error}]."; + ][$error] ?? "Skipping provider [{$providerName}] because it encountered an error [{$error}]."; $e = new SkipProviderException($message, 0, $e); } diff --git a/src/Roots/Acorn/Application/Concerns/Bootable.php b/src/Roots/Acorn/Application/Concerns/Bootable.php new file mode 100644 index 00000000..f8b21660 --- /dev/null +++ b/src/Roots/Acorn/Application/Concerns/Bootable.php @@ -0,0 +1,243 @@ +isBooted()) { + return $this; + } + + if (! defined('LARAVEL_START')) { + define('LARAVEL_START', microtime(true)); + } + + if ($this->runningInConsole()) { + $this->enableHttpsInConsole(); + + class_exists('WP_CLI') ? $this->bootWpCli() : $this->bootConsole(); + + return $this; + } + + $this->bootHttp(); + + return $this; + } + + /** + * Boot the Application for console. + */ + protected function bootConsole(): void + { + $kernel = $this->make(ConsoleKernelContract::class); + + $status = $kernel->handle( + $input = new \Symfony\Component\Console\Input\ArgvInput(), + new \Symfony\Component\Console\Output\ConsoleOutput() + ); + + $kernel->terminate($input, $status); + + exit($status); + } + + /** + * Boot the Application for WP-CLI. + */ + protected function bootWpCli(): void + { + $kernel = $this->make(ConsoleKernelContract::class); + $kernel->bootstrap(); + + WP_CLI::add_command('acorn', function ($args, $options) use ($kernel) { + $kernel->commands(); + + $command = implode(' ', $args); + + foreach ($options as $key => $value) { + if ($key === 'interaction' && $value === false) { + $command .= ' --no-interaction'; + + continue; + } + + $command .= " --{$key}"; + + if ($value !== true) { + $command .= "='{$value}'"; + } + } + + $command = str_replace('\\', '\\\\', $command); + + $status = $kernel->handle( + $input = new \Symfony\Component\Console\Input\StringInput($command), + new \Symfony\Component\Console\Output\ConsoleOutput() + ); + + $kernel->terminate($input, $status); + + WP_CLI::halt($status); + }); + } + + /** + * Boot the Application for HTTP requests. + */ + protected function bootHttp(): void + { + $kernel = $this->make(HttpKernelContract::class); + $request = Request::capture(); + + $this->instance('request', $request); + + Facade::clearResolvedInstance('request'); + + $kernel->bootstrap($request); + + $this->registerDefaultRoute(); + + try { + $route = $this->make('router')->getRoutes()->match($request); + + $this->registerRequestHandler($request, $route); + } catch (Throwable) { + // + } + } + + /** + * Enable `$_SERVER[HTTPS]` in a console environment. + */ + protected function enableHttpsInConsole(): void + { + $enable = apply_filters('acorn/enable_https_in_console', parse_url(get_option('home'), PHP_URL_SCHEME) === 'https'); + + if ($enable) { + $_SERVER['HTTPS'] = 'on'; + } + } + + /** + * Register the default WordPress route. + */ + protected function registerDefaultRoute(): void + { + Route::any('{any?}', fn () => tap(response(''), function (Response $response) { + foreach (headers_list() as $header) { + [$header, $value] = explode(': ', $header, 2); + + if (! headers_sent()) { + header_remove($header); + } + + $response->header($header, $value, $header !== 'Set-Cookie'); + } + + if ($this->hasDebugModeEnabled()) { + $response->header('X-Powered-By', $this->version()); + } + + $content = ''; + + $levels = ob_get_level(); + + for ($i = 0; $i < $levels; $i++) { + $content .= ob_get_clean(); + } + + $response->setContent($content); + })) + ->where('any', '.*') + ->name('wordpress'); + } + + /** + * Register the request handler. + */ + protected function registerRequestHandler( + \Illuminate\Http\Request $request, + ?\Illuminate\Routing\Route $route + ): void { + $path = Str::finish($request->getBaseUrl(), $request->getPathInfo()); + + $except = collect([ + admin_url(), + wp_login_url(), + wp_registration_url(), + ])->map(fn ($url) => parse_url($url, PHP_URL_PATH))->unique()->filter(); + + $api = parse_url(rest_url(), PHP_URL_PATH); + + if ( + Str::startsWith($path, $except->all()) || + Str::endsWith($path, '.php') + ) { + return; + } + + if ( + $isApi = Str::startsWith($path, $api) && + redirect_canonical(null, false) + ) { + return; + } + + add_filter('do_parse_request', function ($condition, $wp, $params) use ($route) { + if (! $route) { + return $condition; + } + + return apply_filters('acorn/router/do_parse_request', $condition, $wp, $params); + }, 100, 3); + + if ($route->getName() !== 'wordpress') { + add_action('parse_request', fn () => $this->handleRequest($request)); + + return; + } + + $config = $this->config->get('router.wordpress', ['web' => 'web', 'api' => 'api']); + + $route->middleware($isApi ? $config['api'] : $config['web']); + + ob_start(); + + remove_action('shutdown', 'wp_ob_end_flush_all', 1); + add_action('shutdown', fn () => $this->handleRequest($request), 100); + } + + /** + * Handle the request. + */ + public function handleRequest(\Illuminate\Http\Request $request): void + { + $kernel = $this->make(HttpKernelContract::class); + + $response = $kernel->handle($request); + + $response->send(); + + $kernel->terminate($request, $response); + + exit((int) $response->isServerError()); + } +} diff --git a/src/Roots/Acorn/Bootloader.php b/src/Roots/Acorn/Bootloader.php deleted file mode 100644 index 55346353..00000000 --- a/src/Roots/Acorn/Bootloader.php +++ /dev/null @@ -1,481 +0,0 @@ -app = $app; - $this->files = $files ?? new Filesystem; - - static::$instance ??= $this; - } - - /** - * Boot the Application. - */ - public function __invoke(): void - { - $this->boot(); - } - - /** - * Set the Bootloader instance. - */ - public static function setInstance(?self $bootloader): void - { - static::$instance = $bootloader; - } - - /** - * Get the Bootloader instance. - */ - public static function getInstance(?ApplicationContract $app = null): static - { - return static::$instance ??= new static($app); - } - - /** - * Boot the Application. - */ - public function boot(?callable $callback = null): void - { - if (! defined('LARAVEL_START')) { - define('LARAVEL_START', microtime(true)); - } - - $this->getApplication(); - - if ($callback) { - $callback($this->app); - } - - if ($this->app->hasBeenBootstrapped()) { - return; - } - - if ($this->app->runningInConsole()) { - $this->enableHttpsInConsole(); - - class_exists('WP_CLI') ? $this->bootWpCli() : $this->bootConsole(); - - return; - } - - $this->bootHttp(); - } - - /** - * Enable `$_SERVER[HTTPS]` in a console environment. - */ - protected function enableHttpsInConsole(): void - { - $enable = apply_filters('acorn/enable_https_in_console', parse_url(get_option('home'), PHP_URL_SCHEME) === 'https'); - - if ($enable) { - $_SERVER['HTTPS'] = 'on'; - } - } - - /** - * Boot the Application for console. - */ - protected function bootConsole(): void - { - $kernel = $this->app->make(\Illuminate\Contracts\Console\Kernel::class); - - $status = $kernel->handle( - $input = new \Symfony\Component\Console\Input\ArgvInput(), - new \Symfony\Component\Console\Output\ConsoleOutput() - ); - - $kernel->terminate($input, $status); - - exit($status); - } - - /** - * Boot the Application for WP-CLI. - */ - protected function bootWpCli(): void - { - $kernel = $this->app->make(\Illuminate\Contracts\Console\Kernel::class); - $kernel->bootstrap(); - - \WP_CLI::add_command('acorn', function ($args, $options) use ($kernel) { - $kernel->commands(); - - $command = implode(' ', $args); - - foreach ($options as $key => $value) { - if ($key === 'interaction' && $value === false) { - $command .= ' --no-interaction'; - - continue; - } - - $command .= " --{$key}"; - - if ($value !== true) { - $command .= "='{$value}'"; - } - } - - $command = str_replace('\\', '\\\\', $command); - - $status = $kernel->handle( - $input = new \Symfony\Component\Console\Input\StringInput($command), - new \Symfony\Component\Console\Output\ConsoleOutput() - ); - - $kernel->terminate($input, $status); - - \WP_CLI::halt($status); - }); - } - - /** - * Boot the Application for HTTP requests. - */ - protected function bootHttp(): void - { - $kernel = $this->app->make(\Illuminate\Contracts\Http\Kernel::class); - $request = \Illuminate\Http\Request::capture(); - - $this->app->instance('request', $request); - - Facade::clearResolvedInstance('request'); - - $kernel->bootstrap($request); - - $this->registerDefaultRoute(); - - try { - $route = $this->app->make('router')->getRoutes()->match($request); - - $this->registerRequestHandler($kernel, $request, $route); - } catch (\Throwable) { - // - } - } - - /** - * Register the default WordPress route. - */ - protected function registerDefaultRoute(): void - { - $this->app->make('router') - ->any('{any?}', fn () => tap(response(''), function (Response $response) { - foreach (headers_list() as $header) { - [$header, $value] = explode(': ', $header, 2); - - if (! headers_sent()) { - header_remove($header); - } - - $response->header($header, $value, $header !== 'Set-Cookie'); - } - - if ($this->app->hasDebugModeEnabled()) { - $response->header('X-Powered-By', $this->app->version()); - } - - $content = ''; - - $levels = ob_get_level(); - - for ($i = 0; $i < $levels; $i++) { - $content .= ob_get_clean(); - } - - $response->setContent($content); - })) - ->where('any', '.*') - ->name('wordpress'); - } - - /** - * Register the request handler. - */ - protected function registerRequestHandler( - \Illuminate\Contracts\Http\Kernel $kernel, - \Illuminate\Http\Request $request, - ?\Illuminate\Routing\Route $route - ): void { - $path = $request->getBaseUrl().$request->getPathInfo(); - - $except = collect([ - admin_url(), - wp_login_url(), - wp_registration_url(), - ])->map(fn ($url) => parse_url($url, PHP_URL_PATH))->unique()->filter(); - - $api = parse_url(rest_url(), PHP_URL_PATH); - - if ( - Str::startsWith($path, $except->all()) || - Str::endsWith($path, '.php') - ) { - return; - } - - if ( - $isApi = Str::startsWith($path, $api) && - redirect_canonical(null, false) - ) { - return; - } - - add_filter('do_parse_request', function ($condition, $wp, $params) use ($route) { - if (! $route) { - return $condition; - } - - return apply_filters('acorn/router/do_parse_request', $condition, $wp, $params); - }, 100, 3); - - if ($route->getName() !== 'wordpress') { - add_action('parse_request', fn () => $this->handleRequest($kernel, $request)); - - return; - } - - if (! $this->shouldHandleDefaultRequest()) { - return; - } - - $config = $this->app->config->get('router.wordpress', ['web' => 'web', 'api' => 'api']); - - $route->middleware($isApi ? $config['api'] : $config['web']); - - ob_start(); - - remove_action('shutdown', 'wp_ob_end_flush_all', 1); - add_action('shutdown', fn () => $this->handleRequest($kernel, $request), 100); - } - - /** - * Handle the request. - */ - protected function handleRequest( - \Illuminate\Contracts\Http\Kernel $kernel, - \Illuminate\Http\Request $request - ): void { - $response = $kernel->handle($request); - - $body = $response->send(); - - $kernel->terminate($request, $body); - - exit((int) $response->isServerError()); - } - - /** - * Determine if the default WordPress request should be handled. - */ - protected function shouldHandleDefaultRequest(): bool - { - return env('ACORN_ENABLE_EXPERIMENTAL_WORDPRESS_REQUEST_HANDLER', false); - } - - /** - * Initialize and retrieve the Application instance. - */ - public function getApplication(): ApplicationContract - { - $this->app ??= new Application($this->basePath(), $this->usePaths()); - - $this->app->useEnvironmentPath($this->environmentPath()); - - $this->app->singleton( - \Illuminate\Contracts\Http\Kernel::class, - \Roots\Acorn\Http\Kernel::class - ); - - $this->app->singleton( - \Illuminate\Contracts\Console\Kernel::class, - \Roots\Acorn\Console\Kernel::class - ); - - $this->app->singleton( - \Illuminate\Contracts\Debug\ExceptionHandler::class, - \Roots\Acorn\Exceptions\Handler::class - ); - - if (class_exists(\Whoops\Run::class)) { - $this->app->bind( - \Illuminate\Contracts\Foundation\ExceptionRenderer::class, - fn (\Illuminate\Contracts\Foundation\Application $app) => $app->make(\Roots\Acorn\Exceptions\Whoops\WhoopsExceptionRenderer::class) - ); - } - - return $this->app; - } - - /** - * Get the application's base path. - */ - protected function basePath(): string - { - if ($this->basePath) { - return $this->basePath; - } - - return $this->basePath = match (true) { - isset($_ENV['APP_BASE_PATH']) => $_ENV['APP_BASE_PATH'], - - defined('ACORN_BASEPATH') => constant('ACORN_BASEPATH'), - - is_file($composerPath = get_theme_file_path('composer.json')) => dirname($composerPath), - - is_dir($appPath = get_theme_file_path('app')) => dirname($appPath), - - is_file($vendorPath = $this->files->closest(dirname(__DIR__, 4), 'composer.json')) => dirname($vendorPath), - - default => dirname(__DIR__, 3) - }; - } - - /** - * Get the environment file path. - */ - protected function environmentPath(): string - { - return is_file($envPath = $this->files->closest($this->basePath(), '.env') ?? '') - ? dirname($envPath) - : $this->basePath(); - } - - /** - * Use paths that are configurable by the developer. - */ - protected function usePaths(): array - { - $paths = []; - - foreach (['app', 'config', 'storage', 'resources', 'public'] as $path) { - $paths[$path] = $this->normalizeApplicationPath($path, null); - } - - $paths['bootstrap'] = $this->normalizeApplicationPath($path, "{$paths['storage']}/framework"); - - return $paths; - } - - /** - * Normalize a relative or absolute path to an application directory. - */ - protected function normalizeApplicationPath(string $path, ?string $default = null): string - { - $key = strtoupper($path); - - if (is_null($env = Env::get("ACORN_{$key}_PATH"))) { - return $default - ?? (defined("ACORN_{$key}_PATH") ? constant("ACORN_{$key}_PATH") : $this->findPath($path)); - } - - return Str::startsWith($env, $this->absoluteApplicationPathPrefixes) - ? $env - : $this->basePath($env); - } - - /** - * Add new prefix to list of absolute path prefixes. - */ - public function addAbsoluteApplicationPathPrefix(string $prefix): self - { - $this->absoluteApplicationPathPrefixes[] = $prefix; - - return $this; - } - - /** - * Find a path that is configurable by the developer. - */ - protected function findPath(string $path): string - { - $path = trim($path, '\\/'); - - $searchPaths = [ - $this->basePath().DIRECTORY_SEPARATOR.$path, - get_theme_file_path($path), - ]; - - return collect($searchPaths) - ->map(fn ($path) => (is_string($path) && is_dir($path)) ? $path : null) - ->filter() - ->whenEmpty(fn ($paths) => $paths->add($this->fallbackPath($path))) - ->unique() - ->first(); - } - - /** - * Fallbacks for path types. - */ - protected function fallbackPath(string $path): string - { - return match ($path) { - 'storage' => $this->fallbackStoragePath(), - 'app' => "{$this->basePath()}/app", - 'public' => "{$this->basePath()}/public", - default => dirname(__DIR__, 3)."/{$path}", - }; - } - - /** - * Ensure that all of the storage directories exist. - */ - protected function fallbackStoragePath(): string - { - $path = Str::finish(WP_CONTENT_DIR, '/cache/acorn'); - - foreach ([ - 'framework/cache/data', - 'framework/views', - 'framework/sessions', - 'logs', - ] as $directory) { - $this->files->ensureDirectoryExists("{$path}/{$directory}", 0755, true); - } - - return $path; - } -} diff --git a/src/Roots/Acorn/Bootstrap/HandleExceptions.php b/src/Roots/Acorn/Bootstrap/HandleExceptions.php index 4cab9b7a..b0d5f27b 100644 --- a/src/Roots/Acorn/Bootstrap/HandleExceptions.php +++ b/src/Roots/Acorn/Bootstrap/HandleExceptions.php @@ -84,7 +84,7 @@ protected function hasHandler() */ protected function renderHttpResponse(Throwable $e) { - if (ob_get_length()) { + for ($i = 0; $i < ob_get_level(); $i++) { ob_end_clean(); } diff --git a/src/Roots/Acorn/Bootstrap/LoadConfiguration.php b/src/Roots/Acorn/Bootstrap/LoadConfiguration.php deleted file mode 100644 index a85671db..00000000 --- a/src/Roots/Acorn/Bootstrap/LoadConfiguration.php +++ /dev/null @@ -1,66 +0,0 @@ -getCachedConfigPath())) { - $items = require $cached; - - $loadedFromCache = true; - } - - // Next we will spin through all of the configuration files in the configuration - // directory and load each one into the repository. This will make all of the - // options available to the developer for use in various parts of this app. - $app->instance('config', $config = new Repository($items)); - - if (! isset($loadedFromCache)) { - $this->loadConfigurationFiles($app, $config); - } - - // Finally, we will set the application's environment based on the configuration - // values that were loaded. We will pass a callback which will be used to get - // the environment in a web context where an "--env" switch is not present. - $app->detectEnvironment(function () use ($config) { - return $config->get('app.env', 'production'); - }); - } - - /** - * Load the configuration items from all of the files. - * - * Fallback to internal app config. - * - * @return void - */ - protected function loadConfigurationFiles(Application $app, RepositoryContract $repository) - { - $files = $this->getConfigurationFiles($app); - - if (! isset($files['app'])) { - $repository->set('app', require dirname(__DIR__, 4).'/config/app.php'); - } - - foreach ($files as $key => $path) { - $repository->set($key, require $path); - } - } -} diff --git a/src/Roots/Acorn/Configuration/ApplicationBuilder.php b/src/Roots/Acorn/Configuration/ApplicationBuilder.php new file mode 100644 index 00000000..072ef959 --- /dev/null +++ b/src/Roots/Acorn/Configuration/ApplicationBuilder.php @@ -0,0 +1,120 @@ +app->singleton( + \Illuminate\Contracts\Http\Kernel::class, + \Roots\Acorn\Http\Kernel::class + ); + + $this->app->singleton( + \Illuminate\Contracts\Console\Kernel::class, + \Roots\Acorn\Console\Kernel::class + ); + + return $this; + } + + /** + * Register and configure the application's exception handler. + * + * @return $this + */ + public function withExceptions(?callable $using = null) + { + $this->app->singleton( + \Illuminate\Contracts\Debug\ExceptionHandler::class, + \Roots\Acorn\Exceptions\Handler::class, + ); + + $using ??= fn () => true; + + $this->app->afterResolving( + \Roots\Acorn\Exceptions\Handler::class, + fn ($handler) => $using(new Exceptions($handler)), + ); + + return $this; + } + + /** + * Register the global middleware, middleware groups, and middleware aliases for the application. + * + * @return $this + */ + public function withMiddleware(?callable $callback = null) + { + $this->app->afterResolving(HttpKernel::class, function ($kernel) use ($callback) { + $middleware = new 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; + } + + /** + * Register additional service providers. + * + * @return $this + */ + public function withProviders(array $providers = [], bool $withBootstrapProviders = true) + { + RegisterProviders::merge( + $providers, + $withBootstrapProviders + ? $this->app->getBootstrapProvidersPath() + : null + ); + + return $this; + } + + /** + * Get the application instance. + * + * @return \Roots\Acorn\Application + */ + public function create() + { + return $this->app; + } + + /** + * Boot the application. + * + * @return \Roots\Acorn\Application + */ + public function boot() + { + return $this->app->bootAcorn(); + } +} diff --git a/src/Roots/Acorn/Configuration/Concerns/Paths.php b/src/Roots/Acorn/Configuration/Concerns/Paths.php new file mode 100644 index 00000000..432f6ba4 --- /dev/null +++ b/src/Roots/Acorn/Configuration/Concerns/Paths.php @@ -0,0 +1,134 @@ + $_ENV['APP_BASE_PATH'], + + defined('ACORN_BASEPATH') => constant('ACORN_BASEPATH'), + + is_file($composerPath = get_theme_file_path('composer.json')) => dirname($composerPath), + + is_dir($appPath = get_theme_file_path('app')) => dirname($appPath), + + optional($vendorPath = (new Filesystem)->closest(dirname(__DIR__, 6), 'composer.json'), 'is_file') => dirname($vendorPath), + + default => dirname(__DIR__, 5), + }; + } + + /** + * Register and configure the application's paths. + * + * @return $this + */ + public function withPaths(?string $app = null, ?string $config = null, ?string $storage = null, ?string $resources = null, ?string $public = null, ?string $bootstrap = null, ?string $lang = null, ?string $database = null) + { + $this->app->usePaths( + array_filter(compact('app', 'config', 'storage', 'resources', 'public', 'bootstrap', 'lang', 'database')) + $this->defaultPaths() + ); + + return $this; + } + + /** + * Use the configured default paths. + */ + public function defaultPaths(): array + { + $paths = []; + + foreach (['app', 'config', 'storage', 'resources', 'public', 'lang', 'database'] as $path) { + $paths[$path] = $this->normalizeApplicationPath($path); + } + + $paths['bootstrap'] = $this->normalizeApplicationPath($path, "{$paths['storage']}/framework"); + + return $paths; + } + + /** + * Normalize a relative or absolute path to an application directory. + */ + protected function normalizeApplicationPath(string $path, ?string $default = null): string + { + $key = strtoupper($path); + + if (is_null($env = Env::get("ACORN_{$key}_PATH"))) { + return $default + ?? (defined("ACORN_{$key}_PATH") ? constant("ACORN_{$key}_PATH") : $this->findPath($path)); + } + + return Str::startsWith($env, $this->app->absoluteCachePathPrefixes) + ? $env + : $this->app->basePath($env); + } + + /** + * Find a path that is configurable by the developer. + */ + protected function findPath(string $path): string + { + $path = trim($path, '\\/'); + + $method = $path === 'app' ? 'path' : "{$path}Path"; + + $searchPaths = [ + method_exists($this->app, $method) ? $this->app->{$method}() : null, + $this->app->basePath($path), + get_theme_file_path($path), + ]; + + return collect($searchPaths) + ->filter(fn ($path) => (is_string($path) && is_dir($path))) + ->whenEmpty(fn ($paths) => $paths->add($this->fallbackPath($path))) + ->unique() + ->first(); + } + + /** + * Fallbacks for path types. + */ + protected function fallbackPath(string $path): string + { + return $path === 'storage' + ? $this->fallbackStoragePath() + : $this->app->basePath($path); + } + + /** + * Ensure that all of the storage directories exist. + */ + protected function fallbackStoragePath(): string + { + $files = new Filesystem; + $path = Str::finish(WP_CONTENT_DIR, '/cache/acorn'); + + foreach ([ + 'framework/cache/data', + 'framework/views', + 'framework/sessions', + 'logs', + ] as $directory) { + $files->ensureDirectoryExists("{$path}/{$directory}", 0755, true); + } + + return $path; + } +} diff --git a/src/Roots/Acorn/Configuration/Exceptions.php b/src/Roots/Acorn/Configuration/Exceptions.php new file mode 100644 index 00000000..50df5fe0 --- /dev/null +++ b/src/Roots/Acorn/Configuration/Exceptions.php @@ -0,0 +1,10 @@ +defaultAliases(), $this->customAliases); + } + + /** + * Get the global middleware. + * + * @return array + */ + public function getGlobalMiddleware() + { + $middleware = $this->global ?: array_values(array_filter([ + $this->trustHosts ? \Illuminate\Http\Middleware\TrustHosts::class : null, + \Illuminate\Http\Middleware\TrustProxies::class, + \Illuminate\Http\Middleware\HandleCors::class, + \Illuminate\Http\Middleware\ValidatePostSize::class, + \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ])); + + $middleware = array_map(function ($middleware) { + return isset($this->replacements[$middleware]) + ? $this->replacements[$middleware] + : $middleware; + }, $middleware); + + return array_values(array_filter( + array_diff( + array_unique(array_merge($this->prepends, $middleware, $this->appends)), + $this->removals + ) + )); + } + + /** + * Get the middleware groups. + * + * @return array + */ + public function getMiddlewareGroups() + { + $middleware = [ + '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, + $this->apiLimiter ? 'throttle:'.$this->apiLimiter : null, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + ])), + ]; + + $middleware = array_merge($middleware, $this->groups); + + foreach ($middleware as $group => $groupedMiddleware) { + foreach ($groupedMiddleware as $index => $groupMiddleware) { + if (isset($this->groupReplacements[$group][$groupMiddleware])) { + $middleware[$group][$index] = $this->groupReplacements[$group][$groupMiddleware]; + } + } + } + + foreach ($this->groupRemovals as $group => $removals) { + $middleware[$group] = array_values(array_filter( + array_diff($middleware[$group] ?? [], $removals) + )); + } + + foreach ($this->groupPrepends as $group => $prepends) { + $middleware[$group] = array_values(array_filter( + array_unique(array_merge($prepends, $middleware[$group] ?? [])) + )); + } + + foreach ($this->groupAppends as $group => $appends) { + $middleware[$group] = array_values(array_filter( + array_unique(array_merge($middleware[$group] ?? [], $appends)) + )); + } + + return $middleware; + } + + /** + * Get the default middleware aliases. + * + * @return array + */ + protected function defaultAliases() + { + $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, + 'throttle' => $this->throttleWithRedis + ? \Illuminate\Routing\Middleware\ThrottleRequestsWithRedis::class + : \Illuminate\Routing\Middleware\ThrottleRequests::class, + // 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, + ]; + + if (class_exists(\Spark\Http\Middleware\VerifyBillableIsSubscribed::class)) { + $aliases['subscribed'] = \Spark\Http\Middleware\VerifyBillableIsSubscribed::class; + } + + return $aliases; + } +} diff --git a/src/Roots/Acorn/Console/Concerns/GetsFreshApplication.php b/src/Roots/Acorn/Console/Concerns/GetsFreshApplication.php index f829c5f3..714abaf9 100644 --- a/src/Roots/Acorn/Console/Concerns/GetsFreshApplication.php +++ b/src/Roots/Acorn/Console/Concerns/GetsFreshApplication.php @@ -2,8 +2,7 @@ namespace Roots\Acorn\Console\Concerns; -use Illuminate\Contracts\Foundation\Application; -use Roots\Acorn\Bootloader; +use Roots\Acorn\Application; trait GetsFreshApplication { @@ -14,13 +13,11 @@ trait GetsFreshApplication */ protected function getFreshApplication() { - $bootloaderClass = get_class(Bootloader::getInstance()); - $applicationClass = get_class($app = Bootloader::getInstance()->getApplication()); + $application = get_class($app = Application::getInstance()); - return (new $bootloaderClass(new $applicationClass( - $app->basePath(), - $this->getApplicationPaths($app) - )))->getApplication(); + return $application::configure($app->basePath()) + ->withPaths(...$this->getApplicationPaths($app)) + ->boot(); } /** @@ -45,14 +42,14 @@ protected function getFreshConfiguration() protected function getApplicationPaths(Application $app) { return [ - 'app' => method_exists($app, 'path') ? $app->path() : $app->make('path'), - 'lang' => method_exists($app, 'langPath') ? $app->langPath() : $app->make('path.lang'), + 'app' => $app->path(), 'config' => $app->configPath(), - 'public' => method_exists($app, 'publicPath') ? $app->publicPath() : $app->make('path.public'), 'storage' => $app->storagePath(), - 'database' => $app->databasePath(), 'resources' => $app->resourcePath(), + 'public' => $app->publicPath(), 'bootstrap' => $app->bootstrapPath(), + 'lang' => $app->langPath(), + 'database' => $app->databasePath(), ]; } } diff --git a/src/Roots/Acorn/Console/Kernel.php b/src/Roots/Acorn/Console/Kernel.php index f189bd15..f7c9267d 100644 --- a/src/Roots/Acorn/Console/Kernel.php +++ b/src/Roots/Acorn/Console/Kernel.php @@ -51,7 +51,7 @@ class Kernel extends FoundationConsoleKernel */ protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, - \Roots\Acorn\Bootstrap\LoadConfiguration::class, + \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Roots\Acorn\Bootstrap\HandleExceptions::class, \Roots\Acorn\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\SetRequestForConsole::class, @@ -74,7 +74,7 @@ public function __construct(Application $app, Dispatcher $events) $this->events = $events; $this->app->booted(function () { - $this->defineConsoleSchedule(); + $this->resolveConsoleSchedule(); }); } diff --git a/src/Roots/Acorn/DefaultProviders.php b/src/Roots/Acorn/DefaultProviders.php index 1f6ad695..947a3766 100644 --- a/src/Roots/Acorn/DefaultProviders.php +++ b/src/Roots/Acorn/DefaultProviders.php @@ -17,7 +17,6 @@ class DefaultProviders extends DefaultProvidersBase \Roots\Acorn\Assets\AssetsServiceProvider::class, \Roots\Acorn\Filesystem\FilesystemServiceProvider::class, \Roots\Acorn\Providers\AcornServiceProvider::class, - \Roots\Acorn\Providers\RouteServiceProvider::class, \Roots\Acorn\View\ViewServiceProvider::class, ]; diff --git a/src/Roots/Acorn/Http/Kernel.php b/src/Roots/Acorn/Http/Kernel.php index 3c815aa8..fe7ae482 100644 --- a/src/Roots/Acorn/Http/Kernel.php +++ b/src/Roots/Acorn/Http/Kernel.php @@ -13,44 +13,10 @@ class Kernel extends HttpKernel */ protected $bootstrappers = [ \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class, - \Roots\Acorn\Bootstrap\LoadConfiguration::class, + \Illuminate\Foundation\Bootstrap\LoadConfiguration::class, \Roots\Acorn\Bootstrap\HandleExceptions::class, \Roots\Acorn\Bootstrap\RegisterFacades::class, \Illuminate\Foundation\Bootstrap\RegisterProviders::class, \Illuminate\Foundation\Bootstrap\BootProviders::class, ]; - - protected $middleware = [ - \Illuminate\Http\Middleware\HandleCors::class, - \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, - \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, - ]; - - protected $middlewareGroups = [ - 'web' => [ - \Illuminate\Session\Middleware\StartSession::class, - \Illuminate\View\Middleware\ShareErrorsFromSession::class, - \Illuminate\Routing\Middleware\SubstituteBindings::class, - ], - - 'api' => [ - // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, - 'throttle:api', - \Illuminate\Routing\Middleware\SubstituteBindings::class, - ], - ]; - - /** - * The application's route middleware. - * - * These middleware may be assigned to groups or used individually. - * - * @var array - */ - protected $routeMiddleware = [ - 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, - 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, - 'can' => \Illuminate\Auth\Middleware\Authorize::class, - 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, - ]; } diff --git a/src/Roots/Acorn/Providers/AcornServiceProvider.php b/src/Roots/Acorn/Providers/AcornServiceProvider.php index 4de6054f..7b45f4a5 100644 --- a/src/Roots/Acorn/Providers/AcornServiceProvider.php +++ b/src/Roots/Acorn/Providers/AcornServiceProvider.php @@ -32,7 +32,6 @@ class AcornServiceProvider extends ServiceProvider \Illuminate\Queue\QueueServiceProvider::class => 'queue', \Illuminate\Session\SessionServiceProvider::class => 'session', \Illuminate\View\ViewServiceProvider::class => 'view', - \Laravel\Sanctum\SanctumServiceProvider::class => 'sanctum', \Roots\Acorn\Assets\AssetsServiceProvider::class => 'assets', ]; @@ -43,7 +42,7 @@ class AcornServiceProvider extends ServiceProvider */ public function register() { - $this->registerConfigs(); + // } /** @@ -59,20 +58,6 @@ public function boot() } } - /** - * Register application configs. - * - * @return void - */ - protected function registerConfigs() - { - $configs = array_merge($this->configs, array_values($this->providerConfigs)); - - foreach ($configs as $config) { - $this->mergeConfigFrom(dirname(__DIR__, 4)."/config/{$config}.php", $config); - } - } - /** * Publish application files. * @@ -91,8 +76,14 @@ protected function registerPublishables() protected function publishConfigs() { foreach ($this->filterPublishableConfigs() as $config) { + $path = dirname(__DIR__, 4); + + $file = file_exists($stub = "{$path}/config-stubs/{$config}.php") + ? $stub + : "{$path}/config/{$config}.php"; + $this->publishes([ - dirname(__DIR__, 4)."/config/{$config}.php" => config_path("{$config}.php"), + $file => config_path("{$config}.php"), ], ['acorn', 'acorn-configs']); } } diff --git a/src/Roots/Acorn/Providers/RouteServiceProvider.php b/src/Roots/Acorn/Providers/RouteServiceProvider.php deleted file mode 100644 index f7c12962..00000000 --- a/src/Roots/Acorn/Providers/RouteServiceProvider.php +++ /dev/null @@ -1,56 +0,0 @@ -configureRateLimiting(); - - $this->routes(function () { - if (is_file($api = base_path('routes/api.php'))) { - Route::middleware('api') - ->prefix('api') - ->group($api); - } - - if (is_file($web = base_path('routes/web.php'))) { - Route::middleware('web') - ->group($web); - } - }); - } - - /** - * Configure the rate limiters for the application. - * - * @return void - */ - protected function configureRateLimiting() - { - RateLimiter::for('api', function (Request $request) { - return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()); - }); - } -} diff --git a/src/Roots/helpers.php b/src/Roots/helpers.php index 0b046431..eff54e44 100644 --- a/src/Roots/helpers.php +++ b/src/Roots/helpers.php @@ -2,11 +2,11 @@ namespace Roots; -use Illuminate\Contracts\Foundation\Application; +use Illuminate\Contracts\Foundation\Application as ApplicationContract; use Illuminate\Contracts\View\Factory as ViewFactory; +use Roots\Acorn\Application; use Roots\Acorn\Assets\Bundle; use Roots\Acorn\Assets\Contracts\Asset; -use Roots\Acorn\Bootloader; /** * Get asset from manifest @@ -34,34 +34,13 @@ function bundle(string $bundle, ?string $manifest = null): Bundle /** * Instantiate the bootloader. + * + * @deprecated Use `Application::configure()->boot()` instead. */ -function bootloader(?Application $app = null): Bootloader +function bootloader(?ApplicationContract $app = null): Application { - $bootloader = Bootloader::getInstance($app); - - /** - * @deprecated - */ - \Roots\add_actions(['after_setup_theme', 'rest_api_init'], function () use ($bootloader) { - $app = $bootloader->getApplication(); - - if ($app->hasBeenBootstrapped()) { - return; - } - - if ($app->runningInConsole()) { - return $bootloader->boot(); - } - - \Roots\wp_die( - 'Acorn failed to boot. Run \\Roots\\bootloader()->boot().

If you\'re using Sage, you need to update sage/functions.php:32', - '\\Roots\\bootloader() was called incorrectly.', - 'Acorn › Boot Error', - 'Check out the release notes for more information.

This message will be removed with the next beta release of Acorn.' - ); - }, 6); - - return $bootloader; + return Application::configure() + ->boot(); } /** diff --git a/tests/Application/ApplicationBuilderTest.php b/tests/Application/ApplicationBuilderTest.php new file mode 100644 index 00000000..0da26f84 --- /dev/null +++ b/tests/Application/ApplicationBuilderTest.php @@ -0,0 +1,36 @@ +fixture('base_path/base_empty'); + $this->assertEquals($path, ApplicationBuilder::inferBasePath()); + unset($_ENV['APP_BASE_PATH']); +}); + +it('uses the ACORN_BASEPATH constant if set', function () { + define('ACORN_BASEPATH', $path = $this->fixture('base_path/base_empty')); + $this->assertEquals($path, ApplicationBuilder::inferBasePath()); +})->skip('This test is skipped because it defines a constant'); + +it('uses the directory of the composer.json file in the theme as the base path', function () { + $this->stub('get_theme_file_path', fn ($path) => $this->fixture("base_path/base_composer/{$path}")); + $path = $this->fixture('base_path/base_composer'); + $this->assertEquals($path, ApplicationBuilder::inferBasePath()); +}); + +it('uses the directory of the app path in the theme as the base path', function () { + $this->stub('get_theme_file_path', fn ($path) => $this->fixture("base_path/base_app/{$path}")); + $path = $this->fixture('base_path/base_app'); + $this->assertEquals($path, ApplicationBuilder::inferBasePath()); +}); + +it('uses acorn directory as fallback base path', function () { + $this->stub('get_theme_file_path', fn ($path) => $this->fixture("base_path/base_empty/{$path}")); + $this->assertEquals(acorn_root(), ApplicationBuilder::inferBasePath()); +}); diff --git a/tests/Application/ApplicationTest.php b/tests/Application/ApplicationTest.php index 00d173ff..83cb1816 100644 --- a/tests/Application/ApplicationTest.php +++ b/tests/Application/ApplicationTest.php @@ -11,7 +11,7 @@ uses(TestCase::class); it('instantiates with custom paths', function () { - $app = new Application(null, [ + $app = (new Application)->usePaths([ 'app' => $this->fixture('use_paths/app'), 'config' => $this->fixture('use_paths/config'), ]); diff --git a/tests/Application/BootloaderTest.php b/tests/Application/BootloaderTest.php deleted file mode 100644 index 39704f44..00000000 --- a/tests/Application/BootloaderTest.php +++ /dev/null @@ -1,65 +0,0 @@ -toBeInstanceOf(Bootloader::class); -}); - -it('should reuse the same instance', function () { - expect(Bootloader::getInstance())->toBe(Bootloader::getInstance()); -}); - -it('should get a new application instance', function () { - expect((new Bootloader)->getApplication())->toBeInstanceOf(\Illuminate\Contracts\Foundation\Application::class); -}); - -it('should set the basePath if env var is set', function () { - $_ENV['APP_BASE_PATH'] = $path = $this->fixture('base_path/base_empty'); - - $app = (new Bootloader)->getApplication(); - - expect($app->basePath())->toBe($path); -}); - -it('should set the basePath if composer.json exists in theme', function () { - $composerPath = $this->fixture('base_path/base_composer'); - - $this->stub('get_theme_file_path', fn ($path) => "{$composerPath}/{$path}"); - - $app = (new Bootloader)->getApplication(); - - expect($app->basePath())->toBe($composerPath); -}); - -it('should set the basePath if app exists in theme', function () { - $appPath = $this->fixture('base_path/base_app'); - $this->stub('get_theme_file_path', fn () => $appPath); - - $app = Bootloader::getInstance()->getApplication(); - - expect($app->basePath())->toBe(dirname($appPath)); -}); - -it('should set the basePath if composer.json exists as ancestor of ../../../', function () { - $files = mock(\Roots\Acorn\Filesystem\Filesystem::class); - $this->stub('get_theme_file_path', fn () => ''); - - $composerPath = $this->fixture('base_path/base_composer'); - - $files->shouldReceive('closest')->andReturn("{$composerPath}/composer.json"); - $files->shouldReceive('ensureDirectoryExists'); - - $app = (new Bootloader(null, $files))->getApplication(); - - expect($app->basePath())->toBe($composerPath); -});