QueueFake.php 18 KB

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