Repository: laravel/framework (php)

Issue:
Deprecations log missing the trace
- Laravel Version: 9.10.0
- PHP Version: 8.1.5

### Description:
Deprecation logs look like this, making it impossible to debug in some cases:
```log
pipeline.WARNING: mb_substr(): Passing null to parameter #1 ($string) of type string is deprecated in /app/vendor/laravel/framework/src/Illuminate/Support/Str.php on line 944  
pipeline.WARNING: mb_substr(): Passing null to parameter #1 ($string) of type string is deprecated in /app/vendor/laravel/framework/src/Illuminate/Support/Str.php on line 944  
pipeline.WARNING: mb_strtoupper(): Passing null to parameter #1 ($string) of type string is deprecated in /app/vendor/laravel/framework/src/Illuminate/Support/Str.php on line 787 
```

It's missing the stack trace, dissimilar to the rest of the exceptions & PHP errors.

### Steps To Reproduce:

1. Clean install Laravel
2. Configure the deprecations logs [as per documentation](https://laravel.com/docs/9.x/logging#logging-deprecation-warnings) by setting an .env variable: `LOG_DEPRECATIONS_CHANNEL=stack`
3. Trigger any PHP deprecation: `str_replace(null, '', '')`
4. See a deprecation log without any stack trace in the log, besides the specific file/line where it occurred: `pipeline.WARNING: str_replace(): Passing null to parameter #1 ($search) of type array|string is deprecated in /app/some/file.php on line 123`

Pull request:
[9.x] Include full stack trace in deprecation
<!--
Please only send a pull request to branches that are currently supported: https://laravel.com/docs/releases#support-policy 

If you are unsure which branch your pull request should be sent to, please read: https://laravel.com/docs/contributions#which-branch

Pull requests without a descriptive title, thorough description, or tests will be closed.

In addition, please describe the benefit to end users; the reasons it does not break any existing features; how it makes building web applications easier, etc.
-->
Closes #42189

Adds full stack trace into deprecations log, similar to other PHP errors.

Patch:
diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php
index 411285ce20..70c3a57b03 100644
--- a/src/Illuminate/Collections/Arr.php
+++ b/src/Illuminate/Collections/Arr.php
@@ -427,6 +427,33 @@ public static function isList($array)
         return ! self::isAssoc($array);
     }
 
+    /**
+     * Join all items using a string. The final items can use a separate glue string.
+     *
+     * @param  array  $array
+     * @param  string  $glue
+     * @param  string  $finalGlue
+     * @return string
+     */
+    public static function join($array, $glue, $finalGlue = '')
+    {
+        if ($finalGlue === '') {
+            return implode($glue, $array);
+        }
+
+        if (count($array) === 0) {
+            return '';
+        }
+
+        if (count($array) === 1) {
+            return end($array);
+        }
+
+        $finalItem = array_pop($array);
+
+        return implode($glue, $array).$finalGlue.$finalItem;
+    }
+
     /**
      * Key an associative array by a field or using a callback.
      *
diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php
index 9a9ea21ac0..af2a18b97a 100755
--- a/src/Illuminate/Database/Connection.php
+++ b/src/Illuminate/Database/Connection.php
@@ -167,7 +167,7 @@ class Connection implements ConnectionInterface
     /**
      * All of the callbacks that should be invoked before a query is executed.
      *
-     * @var array
+     * @var \Closure[]
      */
     protected $beforeExecutingCallbacks = [];
 
@@ -181,14 +181,14 @@ class Connection implements ConnectionInterface
     /**
      * Type mappings that should be registered with new Doctrine connections.
      *
-     * @var array
+     * @var array<string, string>
      */
     protected $doctrineTypeMappings = [];
 
     /**
      * The connection resolvers.
      *
-     * @var array
+     * @var \Closure[]
      */
     protected static $resolvers = [];
 
diff --git a/src/Illuminate/Database/ConnectionResolver.php b/src/Illuminate/Database/ConnectionResolver.php
index ebfc15c94d..dd16ffd657 100755
--- a/src/Illuminate/Database/ConnectionResolver.php
+++ b/src/Illuminate/Database/ConnectionResolver.php
@@ -7,7 +7,7 @@ class ConnectionResolver implements ConnectionResolverInterface
     /**
      * All of the registered connections.
      *
-     * @var array
+     * @var \Illuminate\Database\ConnectionInterface[]
      */
     protected $connections = [];
 
@@ -21,7 +21,7 @@ class ConnectionResolver implements ConnectionResolverInterface
     /**
      * Create a new connection resolver instance.
      *
-     * @param  array  $connections
+     * @param  array<string, \Illuminate\Database\ConnectionInterface>  $connections
      * @return void
      */
     public function __construct(array $connections = [])
diff --git a/src/Illuminate/Database/DatabaseManager.php b/src/Illuminate/Database/DatabaseManager.php
index c23ee42852..4f99071191 100755
--- a/src/Illuminate/Database/DatabaseManager.php
+++ b/src/Illuminate/Database/DatabaseManager.php
@@ -38,14 +38,14 @@ class DatabaseManager implements ConnectionResolverInterface
     /**
      * The active connection instances.
      *
-     * @var array
+     * @var array<string, \Illuminate\Database\Connection>
      */
     protected $connections = [];
 
     /**
      * The custom connection resolvers.
      *
-     * @var array
+     * @var array<string, callable>
      */
     protected $extensions = [];
 
@@ -59,7 +59,7 @@ class DatabaseManager implements ConnectionResolverInterface
     /**
      * The custom Doctrine column types.
      *
-     * @var array
+     * @var array<string, array>
      */
     protected $doctrineTypes = [];
 
@@ -369,7 +369,7 @@ public function setDefaultConnection($name)
     /**
      * Get all of the support drivers.
      *
-     * @return array
+     * @return string[]
      */
     public function supportedDrivers()
     {
@@ -379,7 +379,7 @@ public function supportedDrivers()
     /**
      * Get all of the drivers that are actually available.
      *
-     * @return array
+     * @return string[]
      */
     public function availableDrivers()
     {
@@ -415,7 +415,7 @@ public function forgetExtension($name)
     /**
      * Return all of the created connections.
      *
-     * @return array
+     * @return array<string, \Illuminate\Database\Connection>
      */
     public function getConnections()
     {
diff --git a/src/Illuminate/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php
index 0698b4412b..47081d1af6 100755
--- a/src/Illuminate/Events/Dispatcher.php
+++ b/src/Illuminate/Events/Dispatcher.php
@@ -147,7 +147,7 @@ public function hasWildcardListeners($eventName)
      * Register an event and payload to be fired later.
      *
      * @param  string  $event
-     * @param  array  $payload
+     * @param  object|array  $payload
      * @return void
      */
     public function push($event, $payload = [])
diff --git a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php
index caa2295a75..225ae66dc6 100644
--- a/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php
+++ b/src/Illuminate/Foundation/Bootstrap/HandleExceptions.php
@@ -102,9 +102,7 @@ public function handleDeprecation($message, $file, $line)
         $this->ensureDeprecationLoggerIsConfigured();
 
         with($logger->channel('deprecations'), function ($log) use ($message, $file, $line) {
-            $log->warning(sprintf('%s in %s on line %s',
-                $message, $file, $line
-            ));
+            $log->warning((string) new ErrorException($message, 0, E_DEPRECATED, $file, $line));
         });
     }
 
diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php
index 50b8cbc5da..90a78ae0d7 100644
--- a/src/Illuminate/Foundation/helpers.php
+++ b/src/Illuminate/Foundation/helpers.php
@@ -636,7 +636,7 @@ function report($exception)
      *
      * @param  array|string|null  $key
      * @param  mixed  $default
-     * @return mixed
+     * @return mixed|\Illuminate\Http\Request|string|array|null
      */
     function request($key = null, $default = null)
     {
diff --git a/src/Illuminate/Validation/ConditionalRules.php b/src/Illuminate/Validation/ConditionalRules.php
index f71e99186b..02f8e21fb7 100644
--- a/src/Illuminate/Validation/ConditionalRules.php
+++ b/src/Illuminate/Validation/ConditionalRules.php
@@ -16,14 +16,14 @@ class ConditionalRules
     /**
      * The rules to be added to the attribute.
      *
-     * @var array|string
+     * @var array|string|\Closure
      */
     protected $rules;
 
     /**
      * The rules to be added to the attribute if the condition fails.
      *
-     * @var array|string
+     * @var array|string|\Closure
      */
     protected $defaultRules;
 
@@ -31,8 +31,8 @@ class ConditionalRules
      * Create a new conditional rules instance.
      *
      * @param  callable|bool  $condition
-     * @param  array|string  $rules
-     * @param  array|string  $defaultRules
+     * @param  array|string|\Closure  $rules
+     * @param  array|string|\Closure  $defaultRules
      * @return void
      */
     public function __construct($condition, $rules, $defaultRules = [])
diff --git a/src/Illuminate/Validation/Rule.php b/src/Illuminate/Validation/Rule.php
index 510038a15a..87860cb4f8 100644
--- a/src/Illuminate/Validation/Rule.php
+++ b/src/Illuminate/Validation/Rule.php
@@ -21,8 +21,8 @@ class Rule
      * Create a new conditional rule set.
      *
      * @param  callable|bool  $condition
-     * @param  array|string  $rules
-     * @param  array|string  $defaultRules
+     * @param  array|string|\Closure  $rules
+     * @param  array|string|\Closure  $defaultRules
      * @return \Illuminate\Validation\ConditionalRules
      */
     public static function when($condition, $rules, $defaultRules = [])
diff --git a/src/Illuminate/Validation/ValidationRuleParser.php b/src/Illuminate/Validation/ValidationRuleParser.php
index 653cbd6bd5..e27c730ea8 100644
--- a/src/Illuminate/Validation/ValidationRuleParser.php
+++ b/src/Illuminate/Validation/ValidationRuleParser.php
@@ -97,7 +97,7 @@ protected function explodeExplicitRule($rule, $attribute)
         return array_map(
             [$this, 'prepareRule'],
             $rule,
-            array_fill(array_key_first($rule), count($rule), $attribute)
+            array_fill((int) array_key_first($rule), count($rule), $attribute)
         );
     }
 


Test patch:
diff --git a/src/Illuminate/Support/Testing/Fakes/EventFake.php b/src/Illuminate/Support/Testing/Fakes/EventFake.php
index e5cf53b356..b828dc5e8d 100644
--- a/src/Illuminate/Support/Testing/Fakes/EventFake.php
+++ b/src/Illuminate/Support/Testing/Fakes/EventFake.php
@@ -62,8 +62,12 @@ public function assertListening($expectedEvent, $expectedListener)
             $actualListener = (new ReflectionFunction($listenerClosure))
                         ->getStaticVariables()['listener'];
 
-            if (is_string($actualListener) && Str::endsWith($actualListener, '@handle')) {
-                $actualListener = Str::parseCallback($actualListener)[0];
+            if (is_string($actualListener) && Str::contains($actualListener, '@')) {
+                $actualListener = Str::parseCallback($actualListener);
+
+                if (is_string($expectedListener) && ! Str::contains($expectedListener, '@')) {
+                    $expectedListener = [$expectedListener, 'handle'];
+                }
             }
 
             if ($actualListener === $expectedListener ||
diff --git a/src/Illuminate/Testing/AssertableJsonString.php b/src/Illuminate/Testing/AssertableJsonString.php
index 92962fa55d..54d04b2d45 100644
--- a/src/Illuminate/Testing/AssertableJsonString.php
+++ b/src/Illuminate/Testing/AssertableJsonString.php
@@ -16,7 +16,7 @@ class AssertableJsonString implements ArrayAccess, Countable
     /**
      * The original encoded json.
      *
-     * @var \Illuminate\Contracts\Support\Jsonable|\JsonSerializable|array
+     * @var \Illuminate\Contracts\Support\Jsonable|\JsonSerializable|array|string
      */
     public $json;
 
diff --git a/tests/Database/DatabaseEloquentLocalScopesTest.php b/tests/Database/DatabaseEloquentLocalScopesTest.php
new file mode 100644
index 0000000000..1d71f6f576
--- /dev/null
+++ b/tests/Database/DatabaseEloquentLocalScopesTest.php
@@ -0,0 +1,79 @@
+<?php
+
+namespace Illuminate\Tests\Database;
+
+use Illuminate\Database\Capsule\Manager as DB;
+use Illuminate\Database\Eloquent\Model;
+use PHPUnit\Framework\TestCase;
+
+class DatabaseEloquentLocalScopesTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        tap(new DB)->addConnection([
+            'driver' => 'sqlite',
+            'database' => ':memory:',
+        ])->bootEloquent();
+    }
+
+    protected function tearDown(): void
+    {
+        parent::tearDown();
+
+        Model::unsetConnectionResolver();
+    }
+
+    public function testCanCheckExistenceOfLocalScope()
+    {
+        $model = new EloquentLocalScopesTestModel;
+
+        $this->assertTrue($model->hasNamedScope('active'));
+        $this->assertTrue($model->hasNamedScope('type'));
+
+        $this->assertFalse($model->hasNamedScope('nonExistentLocalScope'));
+    }
+
+    public function testLocalScopeIsApplied()
+    {
+        $model = new EloquentLocalScopesTestModel;
+        $query = $model->newQuery()->active();
+
+        $this->assertSame('select * from "table" where "active" = ?', $query->toSql());
+        $this->assertEquals([true], $query->getBindings());
+    }
+
+    public function testDynamicLocalScopeIsApplied()
+    {
+        $model = new EloquentLocalScopesTestModel;
+        $query = $model->newQuery()->type('foo');
+
+        $this->assertSame('select * from "table" where "type" = ?', $query->toSql());
+        $this->assertEquals(['foo'], $query->getBindings());
+    }
+
+    public function testLocalScopesCanChained()
+    {
+        $model = new EloquentLocalScopesTestModel;
+        $query = $model->newQuery()->active()->type('foo');
+
+        $this->assertSame('select * from "table" where "active" = ? and "type" = ?', $query->toSql());
+        $this->assertEquals([true, 'foo'], $query->getBindings());
+    }
+}
+
+class EloquentLocalScopesTestModel extends Model
+{
+    protected $table = 'table';
+
+    public function scopeActive($query)
+    {
+        $query->where('active', true);
+    }
+
+    public function scopeType($query, $type)
+    {
+        $query->where('type', $type);
+    }
+}
diff --git a/tests/Events/EventsDispatcherTest.php b/tests/Events/EventsDispatcherTest.php
index ea09fe576c..aa352a5bba 100755
--- a/tests/Events/EventsDispatcherTest.php
+++ b/tests/Events/EventsDispatcherTest.php
@@ -178,6 +178,20 @@ public function testMultiplePushedEventsWillGetFlushed()
         $this->assertSame('taylor otwell', $_SERVER['__event.test']);
     }
 
+    public function testPushMethodCanAcceptObjectAsPayload()
+    {
+        unset($_SERVER['__event.test']);
+        $d = new Dispatcher;
+        $d->push(ExampleEvent::class, $e = new ExampleEvent);
+        $d->listen(ExampleEvent::class, function ($payload) {
+            $_SERVER['__event.test'] = $payload;
+        });
+
+        $d->flush(ExampleEvent::class);
+
+        $this->assertSame($e, $_SERVER['__event.test']);
+    }
+
     public function testWildcardListeners()
     {
         unset($_SERVER['__event.test']);
diff --git a/tests/Foundation/Bootstrap/HandleExceptionsTest.php b/tests/Foundation/Bootstrap/HandleExceptionsTest.php
index 458aaf6411..9b06ccd41d 100644
--- a/tests/Foundation/Bootstrap/HandleExceptionsTest.php
+++ b/tests/Foundation/Bootstrap/HandleExceptionsTest.php
@@ -7,6 +7,7 @@
 use Illuminate\Foundation\Application;
 use Illuminate\Foundation\Bootstrap\HandleExceptions;
 use Illuminate\Log\LogManager;
+use Illuminate\Support\Str;
 use Mockery as m;
 use Monolog\Handler\NullHandler;
 use PHPUnit\Framework\TestCase;
@@ -50,11 +51,20 @@ public function testPhpDeprecations()
         $this->app->instance(LogManager::class, $logger);
 
         $logger->shouldReceive('channel')->with('deprecations')->andReturnSelf();
-        $logger->shouldReceive('warning')->with(sprintf('%s in %s on line %s',
-            'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
-            '/home/user/laravel/routes/web.php',
-            17
-        ));
+        $logger->shouldReceive('warning')->with(
+            m::on(fn(string $message) => (bool) preg_match(
+                <<<REGEXP
+                #ErrorException: str_contains\(\): Passing null to parameter \#2 \(\\\$needle\) of type string is deprecated in /home/user/laravel/routes/web\.php:17
+                Stack trace:
+                \#0 .*helpers.php\(.*\): Illuminate\\\\Foundation\\\\Bootstrap\\\\HandleExceptions.*
+                \#1 .*HandleExceptions\.php\(.*\): with.*
+                \#2 .*HandleExceptions\.php\(.*\): Illuminate\\\\Foundation\\\\Bootstrap\\\\HandleExceptions->handleDeprecation.*
+                \#3 .*HandleExceptionsTest\.php\(.*\): Illuminate\\\\Foundation\\\\Bootstrap\\\\HandleExceptions->handleError.*
+                [\s\S]*#i
+                REGEXP,
+                $message
+            ))
+        );
 
         $this->handleExceptions->handleError(
             E_DEPRECATED,
@@ -70,11 +80,20 @@ public function testUserDeprecations()
         $this->app->instance(LogManager::class, $logger);
 
         $logger->shouldReceive('channel')->with('deprecations')->andReturnSelf();
-        $logger->shouldReceive('warning')->with(sprintf('%s in %s on line %s',
-            'str_contains(): Passing null to parameter #2 ($needle) of type string is deprecated',
-            '/home/user/laravel/routes/web.php',
-            17
-        ));
+        $logger->shouldReceive('warning')->with(
+            m::on(fn(string $message) => (bool) preg_match(
+                <<<REGEXP
+                #ErrorException: str_contains\(\): Passing null to parameter \#2 \(\\\$needle\) of type string is deprecated in /home/user/laravel/routes/web\.php:17
+                Stack trace:
+                \#0 .*helpers.php\(.*\): Illuminate\\\\Foundation\\\\Bootstrap\\\\HandleExceptions.*
+                \#1 .*HandleExceptions\.php\(.*\): with.*
+                \#2 .*HandleExceptions\.php\(.*\): Illuminate\\\\Foundation\\\\Bootstrap\\\\HandleExceptions->handleDeprecation.*
+                \#3 .*HandleExceptionsTest\.php\(.*\): Illuminate\\\\Foundation\\\\Bootstrap\\\\HandleExceptions->handleError.*
+                [\s\S]*#i
+                REGEXP,
+                $message
+            ))
+        );
 
         $this->handleExceptions->handleError(
             E_USER_DEPRECATED,
diff --git a/tests/Integration/Events/EventFakeTest.php b/tests/Integration/Events/EventFakeTest.php
index 5964f1b1e6..abc832af68 100644
--- a/tests/Integration/Events/EventFakeTest.php
+++ b/tests/Integration/Events/EventFakeTest.php
@@ -138,6 +138,7 @@ public function testAssertListening()
         Event::assertListening('event', PostAutoEventSubscriber::class);
         Event::assertListening('event', [PostEventSubscriber::class, 'foo']);
         Event::assertListening('post-created', [PostEventSubscriber::class, 'handlePostCreated']);
+        Event::assertListening('post-deleted', [PostEventSubscriber::class, 'handlePostDeleted']);
         Event::assertListening(NonImportantEvent::class, Closure::class);
     }
 }
@@ -158,12 +159,21 @@ public function handlePostCreated($event)
     {
     }
 
+    public function handlePostDeleted($event)
+    {
+    }
+
     public function subscribe($events)
     {
         $events->listen(
             'post-created',
             [PostEventSubscriber::class, 'handlePostCreated']
         );
+
+        $events->listen(
+            'post-deleted',
+            PostEventSubscriber::class.'@handlePostDeleted'
+        );
     }
 }
 
diff --git a/tests/Integration/Routing/FluentRoutingTest.php b/tests/Integration/Routing/FluentRoutingTest.php
index 3d9871e153..bd7e3d484b 100644
--- a/tests/Integration/Routing/FluentRoutingTest.php
+++ b/tests/Integration/Routing/FluentRoutingTest.php
@@ -46,6 +46,21 @@ public function testMiddlewareRunWhenRegisteredAsArrayOrParams()
         $this->assertSame('1_2', $this->get('before_after')->content());
         $this->assertSame('1_2', $this->get('both_before')->content());
         $this->assertSame('1_2', $this->get('both_after')->content());
+        $this->assertSame('1_2', $this->get('both_after')->content());
+    }
+
+    public function testEmptyMiddlewareGroupAreHandledGracefully()
+    {
+        $controller = function () {
+            return 'Hello World';
+        };
+
+        Route::middlewareGroup('public', []);
+
+        Route::middleware('public')
+            ->get('public', $controller);
+
+        $this->assertSame('Hello World', $this->get('public')->content());
     }
 }
 
diff --git a/tests/Session/SessionStoreTest.php b/tests/Session/SessionStoreTest.php
index e2f88c96fc..d423b5a499 100644
--- a/tests/Session/SessionStoreTest.php
+++ b/tests/Session/SessionStoreTest.php
@@ -441,6 +441,21 @@ public function testName()
         $this->assertSame('foo', $session->getName());
     }
 
+    public function testKeyHas()
+    {
+        $session = $this->getSession();
+        $session->put('first_name', 'Mehdi');
+        $session->put('last_name', 'Rajabi');
+
+        $this->assertTrue($session->has('first_name'));
+        $this->assertTrue($session->has('last_name'));
+        $this->assertTrue($session->has('first_name', 'last_name'));
+        $this->assertTrue($session->has(['first_name', 'last_name']));
+
+        $this->assertFalse($session->has('first_name', 'foo'));
+        $this->assertFalse($session->has('foo', 'bar'));
+    }
+
     public function testKeyExists()
     {
         $session = $this->getSession();
diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php
index 77960490f5..94e048646b 100644
--- a/tests/Support/SupportArrTest.php
+++ b/tests/Support/SupportArrTest.php
@@ -212,6 +212,19 @@ public function testFirst()
         $this->assertSame('baz', $value4);
     }
 
+    public function testJoin()
+    {
+        $this->assertSame('a, b, c', Arr::join(['a', 'b', 'c'], ', '));
+
+        $this->assertSame('a, b and c', Arr::join(['a', 'b', 'c'], ', ', ' and '));
+
+        $this->assertSame('a and b', Arr::join(['a', 'b'], ', ', ' and '));
+
+        $this->assertSame('a', Arr::join(['a'], ', ', ' and '));
+
+        $this->assertSame('', Arr::join([], ', ', ' and '));
+    }
+
     public function testLast()
     {
         $array = [100, 200, 300];
diff --git a/tests/Validation/ValidationRuleParserTest.php b/tests/Validation/ValidationRuleParserTest.php
index aff78fbf45..23f6c1d872 100644
--- a/tests/Validation/ValidationRuleParserTest.php
+++ b/tests/Validation/ValidationRuleParserTest.php
@@ -55,6 +55,13 @@ public function testEmptyRulesArePreserved()
         ], $rules);
     }
 
+    public function testEmptyRulesCanBeExploded()
+    {
+        $parser = new ValidationRuleParser(['foo' => 'bar']);
+
+        $this->assertIsObject($parser->explode(['foo' => []]));
+    }
+
     public function testConditionalRulesWithDefault()
     {
         $rules = ValidationRuleParser::filterConditionalRules([