QueueFake.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. <?php
  2. namespace Illuminate\Support\Testing\Fakes;
  3. use BadMethodCallException;
  4. use Closure;
  5. use Illuminate\Bus\UniqueLock;
  6. use Illuminate\Contracts\Cache\Repository as Cache;
  7. use Illuminate\Contracts\Queue\Queue;
  8. use Illuminate\Contracts\Queue\ShouldBeUnique;
  9. use Illuminate\Events\CallQueuedListener;
  10. use Illuminate\Queue\CallQueuedClosure;
  11. use Illuminate\Queue\QueueManager;
  12. use Illuminate\Support\Collection;
  13. use Illuminate\Support\Str;
  14. use Illuminate\Support\Traits\ReflectsClosures;
  15. use PHPUnit\Framework\Assert as PHPUnit;
  16. /**
  17. * @phpstan-type RawPushType array{"payload": string, "queue": string|null, "options": array<array-key, mixed>}
  18. */
  19. class QueueFake extends QueueManager implements Fake, Queue
  20. {
  21. use ReflectsClosures;
  22. /**
  23. * The original queue manager.
  24. *
  25. * @var \Illuminate\Contracts\Queue\Queue
  26. */
  27. public $queue;
  28. /**
  29. * The job types that should be intercepted instead of pushed to the queue.
  30. *
  31. * @var \Illuminate\Support\Collection
  32. */
  33. protected $jobsToFake;
  34. /**
  35. * The job types that should be pushed to the queue and not intercepted.
  36. *
  37. * @var \Illuminate\Support\Collection
  38. */
  39. protected $jobsToBeQueued;
  40. /**
  41. * All of the jobs that have been pushed.
  42. *
  43. * @var array
  44. */
  45. protected $jobs = [];
  46. /**
  47. * All of the payloads that have been raw pushed.
  48. *
  49. * @var list<RawPushType>
  50. */
  51. protected $rawPushes = [];
  52. /**
  53. * All of the unique jobs that were pushed.
  54. *
  55. * @var array
  56. */
  57. private $uniqueJobs = [];
  58. /**
  59. * Indicates if items should be serialized and restored when pushed to the queue.
  60. *
  61. * @var bool
  62. */
  63. protected bool $serializeAndRestore = false;
  64. /**
  65. * Create a new fake queue instance.
  66. *
  67. * @param \Illuminate\Contracts\Foundation\Application $app
  68. * @param array $jobsToFake
  69. * @param \Illuminate\Queue\QueueManager|null $queue
  70. */
  71. public function __construct($app, $jobsToFake = [], $queue = null)
  72. {
  73. parent::__construct($app);
  74. $this->jobsToFake = Collection::wrap($jobsToFake);
  75. $this->jobsToBeQueued = new Collection;
  76. $this->queue = $queue;
  77. }
  78. /**
  79. * Specify the jobs that should be queued instead of faked.
  80. *
  81. * @param array|string $jobsToBeQueued
  82. * @return $this
  83. */
  84. public function except($jobsToBeQueued)
  85. {
  86. $this->jobsToBeQueued = Collection::wrap($jobsToBeQueued)->merge($this->jobsToBeQueued);
  87. return $this;
  88. }
  89. /**
  90. * Assert if a job was pushed based on a truth-test callback.
  91. *
  92. * @param string|\Closure $job
  93. * @param callable|int|null $callback
  94. * @return void
  95. */
  96. public function assertPushed($job, $callback = null)
  97. {
  98. if ($job instanceof Closure) {
  99. [$job, $callback] = [$this->firstClosureParameterType($job), $job];
  100. }
  101. if (is_numeric($callback)) {
  102. return $this->assertPushedTimes($job, $callback);
  103. }
  104. PHPUnit::assertTrue(
  105. $this->pushed($job, $callback)->count() > 0,
  106. "The expected [{$job}] job was not pushed."
  107. );
  108. }
  109. /**
  110. * Assert if a job was pushed a number of times.
  111. *
  112. * @param string $job
  113. * @param int $times
  114. * @return void
  115. */
  116. public function assertPushedTimes($job, $times = 1)
  117. {
  118. $count = $this->pushed($job)->count();
  119. PHPUnit::assertSame(
  120. $times, $count,
  121. sprintf(
  122. "The expected [{$job}] job was pushed {$count} %s instead of {$times} %s.",
  123. Str::plural('time', $count),
  124. Str::plural('time', $times)
  125. )
  126. );
  127. }
  128. /**
  129. * Assert if a job was pushed based on a truth-test callback.
  130. *
  131. * @param string $queue
  132. * @param string|\Closure $job
  133. * @param callable|null $callback
  134. * @return void
  135. */
  136. public function assertPushedOn($queue, $job, $callback = null)
  137. {
  138. if ($job instanceof Closure) {
  139. [$job, $callback] = [$this->firstClosureParameterType($job), $job];
  140. }
  141. $this->assertPushed($job, function ($job, $pushedQueue) use ($callback, $queue) {
  142. if ($pushedQueue !== $queue) {
  143. return false;
  144. }
  145. return $callback ? $callback(...func_get_args()) : true;
  146. });
  147. }
  148. /**
  149. * Assert if a job was pushed with chained jobs based on a truth-test callback.
  150. *
  151. * @param string $job
  152. * @param array $expectedChain
  153. * @param callable|null $callback
  154. * @return void
  155. */
  156. public function assertPushedWithChain($job, $expectedChain = [], $callback = null)
  157. {
  158. PHPUnit::assertTrue(
  159. $this->pushed($job, $callback)->isNotEmpty(),
  160. "The expected [{$job}] job was not pushed."
  161. );
  162. PHPUnit::assertTrue(
  163. (new Collection($expectedChain))->isNotEmpty(),
  164. 'The expected chain can not be empty.'
  165. );
  166. $this->isChainOfObjects($expectedChain)
  167. ? $this->assertPushedWithChainOfObjects($job, $expectedChain, $callback)
  168. : $this->assertPushedWithChainOfClasses($job, $expectedChain, $callback);
  169. }
  170. /**
  171. * Assert if a job was pushed with an empty chain based on a truth-test callback.
  172. *
  173. * @param string $job
  174. * @param callable|null $callback
  175. * @return void
  176. */
  177. public function assertPushedWithoutChain($job, $callback = null)
  178. {
  179. PHPUnit::assertTrue(
  180. $this->pushed($job, $callback)->isNotEmpty(),
  181. "The expected [{$job}] job was not pushed."
  182. );
  183. $this->assertPushedWithChainOfClasses($job, [], $callback);
  184. }
  185. /**
  186. * Assert if a job was pushed with chained jobs based on a truth-test callback.
  187. *
  188. * @param string $job
  189. * @param array $expectedChain
  190. * @param callable|null $callback
  191. * @return void
  192. */
  193. protected function assertPushedWithChainOfObjects($job, $expectedChain, $callback)
  194. {
  195. $chain = (new Collection($expectedChain))->map(fn ($job) => serialize($job))->all();
  196. PHPUnit::assertTrue(
  197. $this->pushed($job, $callback)->filter(fn ($job) => $job->chained == $chain)->isNotEmpty(),
  198. 'The expected chain was not pushed.'
  199. );
  200. }
  201. /**
  202. * Assert if a job was pushed with chained jobs based on a truth-test callback.
  203. *
  204. * @param string $job
  205. * @param array $expectedChain
  206. * @param callable|null $callback
  207. * @return void
  208. */
  209. protected function assertPushedWithChainOfClasses($job, $expectedChain, $callback)
  210. {
  211. $matching = $this->pushed($job, $callback)->map->chained->map(function ($chain) {
  212. return (new Collection($chain))->map(function ($job) {
  213. return get_class(unserialize($job));
  214. });
  215. })->filter(function ($chain) use ($expectedChain) {
  216. return $chain->all() === $expectedChain;
  217. });
  218. PHPUnit::assertTrue(
  219. $matching->isNotEmpty(), 'The expected chain was not pushed.'
  220. );
  221. }
  222. /**
  223. * Assert if a closure was pushed based on a truth-test callback.
  224. *
  225. * @param callable|int|null $callback
  226. * @return void
  227. */
  228. public function assertClosurePushed($callback = null)
  229. {
  230. $this->assertPushed(CallQueuedClosure::class, $callback);
  231. }
  232. /**
  233. * Assert that a closure was not pushed based on a truth-test callback.
  234. *
  235. * @param callable|null $callback
  236. * @return void
  237. */
  238. public function assertClosureNotPushed($callback = null)
  239. {
  240. $this->assertNotPushed(CallQueuedClosure::class, $callback);
  241. }
  242. /**
  243. * Determine if the given chain is entirely composed of objects.
  244. *
  245. * @param array $chain
  246. * @return bool
  247. */
  248. protected function isChainOfObjects($chain)
  249. {
  250. return ! (new Collection($chain))->contains(fn ($job) => ! is_object($job));
  251. }
  252. /**
  253. * Determine if a job was pushed based on a truth-test callback.
  254. *
  255. * @param string|\Closure $job
  256. * @param callable|null $callback
  257. * @return void
  258. */
  259. public function assertNotPushed($job, $callback = null)
  260. {
  261. if ($job instanceof Closure) {
  262. [$job, $callback] = [$this->firstClosureParameterType($job), $job];
  263. }
  264. PHPUnit::assertCount(
  265. 0, $this->pushed($job, $callback),
  266. "The unexpected [{$job}] job was pushed."
  267. );
  268. }
  269. /**
  270. * Assert the total count of jobs that were pushed.
  271. *
  272. * @param int $expectedCount
  273. * @return void
  274. */
  275. public function assertCount($expectedCount)
  276. {
  277. $actualCount = (new Collection($this->jobs))->flatten(1)->count();
  278. PHPUnit::assertSame(
  279. $expectedCount, $actualCount,
  280. "Expected {$expectedCount} jobs to be pushed, but found {$actualCount} instead."
  281. );
  282. }
  283. /**
  284. * Assert that no jobs were pushed.
  285. *
  286. * @return void
  287. */
  288. public function assertNothingPushed()
  289. {
  290. $pushedJobs = implode("\n- ", array_keys($this->jobs));
  291. PHPUnit::assertEmpty($this->jobs, "The following jobs were pushed unexpectedly:\n\n- $pushedJobs\n");
  292. }
  293. /**
  294. * Get all of the jobs matching a truth-test callback.
  295. *
  296. * @param string $job
  297. * @param callable|null $callback
  298. * @return \Illuminate\Support\Collection
  299. */
  300. public function pushed($job, $callback = null)
  301. {
  302. if (! $this->hasPushed($job)) {
  303. return new Collection;
  304. }
  305. $callback = $callback ?: fn () => true;
  306. return (new Collection($this->jobs[$job]))->filter(
  307. fn ($data) => $callback($data['job'], $data['queue'], $data['data'])
  308. )->pluck('job');
  309. }
  310. /**
  311. * Get all of the raw pushes matching a truth-test callback.
  312. *
  313. * @param null|\Closure(string, ?string, array): bool $callback
  314. * @return \Illuminate\Support\Collection<int, RawPushType>
  315. */
  316. public function pushedRaw($callback = null)
  317. {
  318. $callback ??= static fn () => true;
  319. return (new Collection($this->rawPushes))->filter(fn ($data) => $callback($data['payload'], $data['queue'], $data['options']));
  320. }
  321. /**
  322. * Get all of the jobs by listener class, passing an optional truth-test callback.
  323. *
  324. * @param class-string $listenerClass
  325. * @param (\Closure(mixed, \Illuminate\Events\CallQueuedListener, string|null, mixed): bool)|null $callback
  326. * @return \Illuminate\Support\Collection<int, \Illuminate\Events\CallQueuedListener>
  327. */
  328. public function listenersPushed($listenerClass, $callback = null)
  329. {
  330. if (! $this->hasPushed(CallQueuedListener::class)) {
  331. return new Collection;
  332. }
  333. $collection = (new Collection($this->jobs[CallQueuedListener::class]))
  334. ->filter(fn ($data) => $data['job']->class === $listenerClass);
  335. if ($callback) {
  336. $collection = $collection->filter(fn ($data) => $callback($data['job']->data[0] ?? null, $data['job'], $data['queue'], $data['data']));
  337. }
  338. return $collection->pluck('job');
  339. }
  340. /**
  341. * Determine if there are any stored jobs for a given class.
  342. *
  343. * @param string $job
  344. * @return bool
  345. */
  346. public function hasPushed($job)
  347. {
  348. return isset($this->jobs[$job]) && ! empty($this->jobs[$job]);
  349. }
  350. /**
  351. * Resolve a queue connection instance.
  352. *
  353. * @param mixed $value
  354. * @return \Illuminate\Contracts\Queue\Queue
  355. */
  356. public function connection($value = null)
  357. {
  358. return $this;
  359. }
  360. /**
  361. * Get the size of the queue.
  362. *
  363. * @param string|null $queue
  364. * @return int
  365. */
  366. public function size($queue = null)
  367. {
  368. return (new Collection($this->jobs))
  369. ->flatten(1)
  370. ->filter(fn ($job) => $job['queue'] === $queue)
  371. ->count();
  372. }
  373. /**
  374. * Get the number of pending jobs.
  375. *
  376. * @param string|null $queue
  377. * @return int
  378. */
  379. public function pendingSize($queue = null)
  380. {
  381. return $this->size($queue);
  382. }
  383. /**
  384. * Get the number of delayed jobs.
  385. *
  386. * @param string|null $queue
  387. * @return int
  388. */
  389. public function delayedSize($queue = null)
  390. {
  391. return 0;
  392. }
  393. /**
  394. * Get the number of reserved jobs.
  395. *
  396. * @param string|null $queue
  397. * @return int
  398. */
  399. public function reservedSize($queue = null)
  400. {
  401. return 0;
  402. }
  403. /**
  404. * Get the creation timestamp of the oldest pending job, excluding delayed jobs.
  405. *
  406. * @param string|null $queue
  407. * @return int|null
  408. */
  409. public function creationTimeOfOldestPendingJob($queue = null)
  410. {
  411. return null;
  412. }
  413. /**
  414. * Push a new job onto the queue.
  415. *
  416. * @param string|object $job
  417. * @param mixed $data
  418. * @param string|null $queue
  419. * @return mixed
  420. */
  421. public function push($job, $data = '', $queue = null)
  422. {
  423. if ($this->shouldFakeJob($job)) {
  424. if ($job instanceof Closure) {
  425. $job = CallQueuedClosure::create($job);
  426. }
  427. $this->jobs[is_object($job) ? get_class($job) : $job][] = [
  428. 'job' => $this->serializeAndRestore ? $this->serializeAndRestoreJob($job) : $job,
  429. 'queue' => $queue,
  430. 'data' => $data,
  431. ];
  432. if ($job instanceof ShouldBeUnique) {
  433. $this->uniqueJobs[] = $job;
  434. }
  435. } else {
  436. is_object($job) && isset($job->connection)
  437. ? $this->queue->connection($job->connection)->push($job, $data, $queue)
  438. : $this->queue->push($job, $data, $queue);
  439. }
  440. }
  441. /**
  442. * Determine if a job should be faked or actually dispatched.
  443. *
  444. * @param object $job
  445. * @return bool
  446. */
  447. public function shouldFakeJob($job)
  448. {
  449. if ($this->shouldDispatchJob($job)) {
  450. return false;
  451. }
  452. if ($this->jobsToFake->isEmpty()) {
  453. return true;
  454. }
  455. return $this->jobsToFake->contains(
  456. fn ($jobToFake) => $job instanceof ((string) $jobToFake) || $job === (string) $jobToFake
  457. );
  458. }
  459. /**
  460. * Determine if a job should be pushed to the queue instead of faked.
  461. *
  462. * @param object $job
  463. * @return bool
  464. */
  465. protected function shouldDispatchJob($job)
  466. {
  467. if ($this->jobsToBeQueued->isEmpty()) {
  468. return false;
  469. }
  470. return $this->jobsToBeQueued->contains(
  471. fn ($jobToQueue) => $job instanceof ((string) $jobToQueue)
  472. );
  473. }
  474. /**
  475. * Push a raw payload onto the queue.
  476. *
  477. * @param string $payload
  478. * @param string|null $queue
  479. * @param array $options
  480. * @return mixed
  481. */
  482. public function pushRaw($payload, $queue = null, array $options = [])
  483. {
  484. $this->rawPushes[] = [
  485. 'payload' => $payload,
  486. 'queue' => $queue,
  487. 'options' => $options,
  488. ];
  489. }
  490. /**
  491. * Push a new job onto the queue after (n) seconds.
  492. *
  493. * @param \DateTimeInterface|\DateInterval|int $delay
  494. * @param string|object $job
  495. * @param mixed $data
  496. * @param string|null $queue
  497. * @return mixed
  498. */
  499. public function later($delay, $job, $data = '', $queue = null)
  500. {
  501. return $this->push($job, $data, $queue);
  502. }
  503. /**
  504. * Push a new job onto the queue.
  505. *
  506. * @param string $queue
  507. * @param string|object $job
  508. * @param mixed $data
  509. * @return mixed
  510. */
  511. public function pushOn($queue, $job, $data = '')
  512. {
  513. return $this->push($job, $data, $queue);
  514. }
  515. /**
  516. * Push a new job onto a specific queue after (n) seconds.
  517. *
  518. * @param string $queue
  519. * @param \DateTimeInterface|\DateInterval|int $delay
  520. * @param string|object $job
  521. * @param mixed $data
  522. * @return mixed
  523. */
  524. public function laterOn($queue, $delay, $job, $data = '')
  525. {
  526. return $this->push($job, $data, $queue);
  527. }
  528. /**
  529. * Pop the next job off of the queue.
  530. *
  531. * @param string|null $queue
  532. * @return \Illuminate\Contracts\Queue\Job|null
  533. */
  534. public function pop($queue = null)
  535. {
  536. //
  537. }
  538. /**
  539. * Push an array of jobs onto the queue.
  540. *
  541. * @param array $jobs
  542. * @param mixed $data
  543. * @param string|null $queue
  544. * @return mixed
  545. */
  546. public function bulk($jobs, $data = '', $queue = null)
  547. {
  548. foreach ($jobs as $job) {
  549. $this->push($job, $data, $queue);
  550. }
  551. }
  552. /**
  553. * Get the jobs that have been pushed.
  554. *
  555. * @return array
  556. */
  557. public function pushedJobs()
  558. {
  559. return $this->jobs;
  560. }
  561. /**
  562. * Get the payloads that were pushed raw.
  563. *
  564. * @return list<RawPushType>
  565. */
  566. public function rawPushes()
  567. {
  568. return $this->rawPushes;
  569. }
  570. /**
  571. * Specify if jobs should be serialized and restored when being "pushed" to the queue.
  572. *
  573. * @param bool $serializeAndRestore
  574. * @return $this
  575. */
  576. public function serializeAndRestore(bool $serializeAndRestore = true)
  577. {
  578. $this->serializeAndRestore = $serializeAndRestore;
  579. return $this;
  580. }
  581. /**
  582. * Serialize and unserialize the job to simulate the queueing process.
  583. *
  584. * @param mixed $job
  585. * @return mixed
  586. */
  587. protected function serializeAndRestoreJob($job)
  588. {
  589. return unserialize(serialize($job));
  590. }
  591. /**
  592. * Release the locks for all unique jobs that were pushed.
  593. *
  594. * @return void
  595. */
  596. public function releaseUniqueJobLocks()
  597. {
  598. $lock = new UniqueLock($this->app->make(Cache::class));
  599. foreach ($this->uniqueJobs as $job) {
  600. $lock->release($job);
  601. }
  602. $this->uniqueJobs = [];
  603. }
  604. /**
  605. * Get the connection name for the queue.
  606. *
  607. * @return string
  608. */
  609. public function getConnectionName()
  610. {
  611. //
  612. }
  613. /**
  614. * Set the connection name for the queue.
  615. *
  616. * @param string $name
  617. * @return $this
  618. */
  619. public function setConnectionName($name)
  620. {
  621. return $this;
  622. }
  623. /**
  624. * Override the QueueManager to prevent circular dependency.
  625. *
  626. * @param string $method
  627. * @param array $parameters
  628. * @return mixed
  629. *
  630. * @throws \BadMethodCallException
  631. */
  632. public function __call($method, $parameters)
  633. {
  634. throw new BadMethodCallException(sprintf(
  635. 'Call to undefined method %s::%s()', static::class, $method
  636. ));
  637. }
  638. }