PendingBatch.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. <?php
  2. namespace Illuminate\Bus;
  3. use Closure;
  4. use Illuminate\Bus\Events\BatchDispatched;
  5. use Illuminate\Contracts\Container\Container;
  6. use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
  7. use Illuminate\Support\Arr;
  8. use Illuminate\Support\Collection;
  9. use Illuminate\Support\Traits\Conditionable;
  10. use Laravel\SerializableClosure\SerializableClosure;
  11. use RuntimeException;
  12. use Throwable;
  13. use UnitEnum;
  14. use function Illuminate\Support\enum_value;
  15. class PendingBatch
  16. {
  17. use Conditionable;
  18. /**
  19. * The IoC container instance.
  20. *
  21. * @var \Illuminate\Contracts\Container\Container
  22. */
  23. protected $container;
  24. /**
  25. * The batch name.
  26. *
  27. * @var string
  28. */
  29. public $name = '';
  30. /**
  31. * The jobs that belong to the batch.
  32. *
  33. * @var \Illuminate\Support\Collection
  34. */
  35. public $jobs;
  36. /**
  37. * The batch options.
  38. *
  39. * @var array
  40. */
  41. public $options = [];
  42. /**
  43. * Jobs that have been verified to contain the Batchable trait.
  44. *
  45. * @var array<class-string, bool>
  46. */
  47. protected static $batchableClasses = [];
  48. /**
  49. * Create a new pending batch instance.
  50. *
  51. * @param \Illuminate\Contracts\Container\Container $container
  52. * @param \Illuminate\Support\Collection $jobs
  53. */
  54. public function __construct(Container $container, Collection $jobs)
  55. {
  56. $this->container = $container;
  57. $this->jobs = $jobs->filter()->values()->each(function (object|array $job) {
  58. $this->ensureJobIsBatchable($job);
  59. });
  60. }
  61. /**
  62. * Add jobs to the batch.
  63. *
  64. * @param iterable|object|array $jobs
  65. * @return $this
  66. */
  67. public function add($jobs)
  68. {
  69. $jobs = is_iterable($jobs) ? $jobs : Arr::wrap($jobs);
  70. foreach ($jobs as $job) {
  71. $this->ensureJobIsBatchable($job);
  72. $this->jobs->push($job);
  73. }
  74. return $this;
  75. }
  76. /**
  77. * Ensure the given job is batchable.
  78. *
  79. * @param object|array $job
  80. * @return void
  81. */
  82. protected function ensureJobIsBatchable(object|array $job): void
  83. {
  84. foreach (Arr::wrap($job) as $job) {
  85. if ($job instanceof PendingBatch || $job instanceof Closure) {
  86. return;
  87. }
  88. if (! (static::$batchableClasses[$job::class] ?? false) && ! in_array(Batchable::class, class_uses_recursive($job))) {
  89. static::$batchableClasses[$job::class] = false;
  90. throw new RuntimeException(sprintf('Attempted to batch job [%s], but it does not use the Batchable trait.', $job::class));
  91. }
  92. static::$batchableClasses[$job::class] = true;
  93. }
  94. }
  95. /**
  96. * Add a callback to be executed when the batch is stored.
  97. *
  98. * @param callable $callback
  99. * @return $this
  100. */
  101. public function before($callback)
  102. {
  103. $this->registerCallback('before', $callback);
  104. return $this;
  105. }
  106. /**
  107. * Get the "before" callbacks that have been registered with the pending batch.
  108. *
  109. * @return array
  110. */
  111. public function beforeCallbacks()
  112. {
  113. return $this->options['before'] ?? [];
  114. }
  115. /**
  116. * Add a callback to be executed after a job in the batch have executed successfully.
  117. *
  118. * @param callable $callback
  119. * @return $this
  120. */
  121. public function progress($callback)
  122. {
  123. $this->registerCallback('progress', $callback);
  124. return $this;
  125. }
  126. /**
  127. * Get the "progress" callbacks that have been registered with the pending batch.
  128. *
  129. * @return array
  130. */
  131. public function progressCallbacks()
  132. {
  133. return $this->options['progress'] ?? [];
  134. }
  135. /**
  136. * Add a callback to be executed after all jobs in the batch have executed successfully.
  137. *
  138. * @param callable $callback
  139. * @return $this
  140. */
  141. public function then($callback)
  142. {
  143. $this->registerCallback('then', $callback);
  144. return $this;
  145. }
  146. /**
  147. * Get the "then" callbacks that have been registered with the pending batch.
  148. *
  149. * @return array
  150. */
  151. public function thenCallbacks()
  152. {
  153. return $this->options['then'] ?? [];
  154. }
  155. /**
  156. * Add a callback to be executed after the first failing job in the batch.
  157. *
  158. * @param callable $callback
  159. * @return $this
  160. */
  161. public function catch($callback)
  162. {
  163. $this->registerCallback('catch', $callback);
  164. return $this;
  165. }
  166. /**
  167. * Get the "catch" callbacks that have been registered with the pending batch.
  168. *
  169. * @return array
  170. */
  171. public function catchCallbacks()
  172. {
  173. return $this->options['catch'] ?? [];
  174. }
  175. /**
  176. * Add a callback to be executed after the batch has finished executing.
  177. *
  178. * @param callable $callback
  179. * @return $this
  180. */
  181. public function finally($callback)
  182. {
  183. $this->registerCallback('finally', $callback);
  184. return $this;
  185. }
  186. /**
  187. * Get the "finally" callbacks that have been registered with the pending batch.
  188. *
  189. * @return array
  190. */
  191. public function finallyCallbacks()
  192. {
  193. return $this->options['finally'] ?? [];
  194. }
  195. /**
  196. * Indicate that the batch should not be canceled when a job within the batch fails.
  197. *
  198. * Optionally, add callbacks to be executed upon each job failure.
  199. *
  200. * @phpstan-type TParam (Closure(\Illuminate\Bus\Batch, \Throwable|null): mixed)|(callable(\Illuminate\Bus\Batch, \Throwable|null): mixed)
  201. *
  202. * @param bool|TParam|array<array-key, TParam> $param
  203. * @return $this
  204. */
  205. public function allowFailures($param = true)
  206. {
  207. if (! is_bool($param)) {
  208. $param = Arr::wrap($param);
  209. foreach ($param as $callback) {
  210. if (is_callable($callback)) {
  211. $this->registerCallback('failure', $callback);
  212. }
  213. }
  214. }
  215. $this->options['allowFailures'] = ! ($param === false);
  216. return $this;
  217. }
  218. /**
  219. * Determine if the pending batch allows jobs to fail without cancelling the batch.
  220. *
  221. * @return bool
  222. */
  223. public function allowsFailures()
  224. {
  225. return Arr::get($this->options, 'allowFailures', false) === true;
  226. }
  227. /**
  228. * Get the "failure" callbacks that have been registered with the pending batch.
  229. *
  230. * @return array<array-key, Closure|callable>
  231. */
  232. public function failureCallbacks(): array
  233. {
  234. return $this->options['failure'] ?? [];
  235. }
  236. /**
  237. * Register a callback with proper serialization.
  238. */
  239. private function registerCallback(string $type, Closure|callable $callback): void
  240. {
  241. $this->options[$type][] = $callback instanceof Closure
  242. ? new SerializableClosure($callback)
  243. : $callback;
  244. }
  245. /**
  246. * Set the name for the batch.
  247. *
  248. * @param string $name
  249. * @return $this
  250. */
  251. public function name(string $name)
  252. {
  253. $this->name = $name;
  254. return $this;
  255. }
  256. /**
  257. * Specify the queue connection that the batched jobs should run on.
  258. *
  259. * @param \UnitEnum|string $connection
  260. * @return $this
  261. */
  262. public function onConnection(UnitEnum|string $connection)
  263. {
  264. $this->options['connection'] = enum_value($connection);
  265. return $this;
  266. }
  267. /**
  268. * Get the connection used by the pending batch.
  269. *
  270. * @return string|null
  271. */
  272. public function connection()
  273. {
  274. return $this->options['connection'] ?? null;
  275. }
  276. /**
  277. * Specify the queue that the batched jobs should run on.
  278. *
  279. * @param \UnitEnum|string|null $queue
  280. * @return $this
  281. */
  282. public function onQueue($queue)
  283. {
  284. $this->options['queue'] = enum_value($queue);
  285. return $this;
  286. }
  287. /**
  288. * Get the queue used by the pending batch.
  289. *
  290. * @return string|null
  291. */
  292. public function queue()
  293. {
  294. return $this->options['queue'] ?? null;
  295. }
  296. /**
  297. * Add additional data into the batch's options array.
  298. *
  299. * @param string $key
  300. * @param mixed $value
  301. * @return $this
  302. */
  303. public function withOption(string $key, $value)
  304. {
  305. $this->options[$key] = $value;
  306. return $this;
  307. }
  308. /**
  309. * Dispatch the batch.
  310. *
  311. * @return \Illuminate\Bus\Batch
  312. *
  313. * @throws \Throwable
  314. */
  315. public function dispatch()
  316. {
  317. $repository = $this->container->make(BatchRepository::class);
  318. try {
  319. $batch = $this->store($repository);
  320. $batch = $batch->add($this->jobs);
  321. } catch (Throwable $e) {
  322. if (isset($batch)) {
  323. $repository->delete($batch->id);
  324. }
  325. throw $e;
  326. }
  327. $this->container->make(EventDispatcher::class)->dispatch(
  328. new BatchDispatched($batch)
  329. );
  330. return $batch;
  331. }
  332. /**
  333. * Dispatch the batch after the response is sent to the browser.
  334. *
  335. * @return \Illuminate\Bus\Batch
  336. */
  337. public function dispatchAfterResponse()
  338. {
  339. $repository = $this->container->make(BatchRepository::class);
  340. $batch = $this->store($repository);
  341. if ($batch) {
  342. $this->container->terminating(function () use ($batch) {
  343. $this->dispatchExistingBatch($batch);
  344. });
  345. }
  346. return $batch;
  347. }
  348. /**
  349. * Dispatch an existing batch.
  350. *
  351. * @param \Illuminate\Bus\Batch $batch
  352. * @return void
  353. *
  354. * @throws \Throwable
  355. */
  356. protected function dispatchExistingBatch($batch)
  357. {
  358. try {
  359. $batch = $batch->add($this->jobs);
  360. } catch (Throwable $e) {
  361. $batch->delete();
  362. throw $e;
  363. }
  364. $this->container->make(EventDispatcher::class)->dispatch(
  365. new BatchDispatched($batch)
  366. );
  367. }
  368. /**
  369. * Dispatch the batch if the given truth test passes.
  370. *
  371. * @param bool|\Closure $boolean
  372. * @return \Illuminate\Bus\Batch|null
  373. */
  374. public function dispatchIf($boolean)
  375. {
  376. return value($boolean) ? $this->dispatch() : null;
  377. }
  378. /**
  379. * Dispatch the batch unless the given truth test passes.
  380. *
  381. * @param bool|\Closure $boolean
  382. * @return \Illuminate\Bus\Batch|null
  383. */
  384. public function dispatchUnless($boolean)
  385. {
  386. return ! value($boolean) ? $this->dispatch() : null;
  387. }
  388. /**
  389. * Store the batch using the given repository.
  390. *
  391. * @param \Illuminate\Bus\BatchRepository $repository
  392. * @return \Illuminate\Bus\Batch
  393. */
  394. protected function store($repository)
  395. {
  396. $batch = $repository->store($this);
  397. (new Collection($this->beforeCallbacks()))->each(function ($handler) use ($batch) {
  398. try {
  399. return $handler($batch);
  400. } catch (Throwable $e) {
  401. if (function_exists('report')) {
  402. report($e);
  403. }
  404. }
  405. });
  406. return $batch;
  407. }
  408. }