Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.04% covered (warning)
87.04%
47 / 54
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
JobFactory
87.04% covered (warning)
87.04%
47 / 54
33.33% covered (danger)
33.33%
1 / 3
15.49
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 newJob
96.67% covered (success)
96.67%
29 / 30
0.00% covered (danger)
0.00%
0 / 1
7
 needsTitle
72.73% covered (warning)
72.73%
16 / 22
0.00% covered (danger)
0.00%
0 / 1
7.99
1<?php
2
3namespace MediaWiki\JobQueue;
4
5use Closure;
6use InvalidArgumentException;
7use MediaWiki\Page\PageReference;
8use MediaWiki\Title\Title;
9use Wikimedia\ObjectFactory\ObjectFactory;
10
11/**
12 * @since 1.40
13 */
14class JobFactory {
15
16    private ObjectFactory $objectFactory;
17
18    /** @var array<array|callable|string> Object specs, see ObjectFactory */
19    private array $jobObjectSpecs;
20
21    /**
22     * @param ObjectFactory $objectFactory
23     * @param array<array|callable|string> $jobObjectSpecs Object specs, see ObjectFactory
24     */
25    public function __construct( ObjectFactory $objectFactory, array $jobObjectSpecs ) {
26        $this->objectFactory = $objectFactory;
27        $this->jobObjectSpecs = $jobObjectSpecs;
28    }
29
30    /**
31     * Create the appropriate object to handle a specific job.
32     *
33     * @note For backwards compatibility with Job::factory,
34     * this method also supports an alternative signature:
35     * @code
36     *   newJob(
37     *     string $command,
38     *     PageReference $page,
39     *     array $params
40     *   )
41     * @endcode
42     *
43     * @param string $command Job command
44     * @param array $params Job parameters
45     *
46     * @return Job
47     * @throws InvalidArgumentException
48     */
49    public function newJob( string $command, $params = [] ): Job {
50        if ( !isset( $this->jobObjectSpecs[ $command ] ) ) {
51            throw new InvalidArgumentException( "Invalid job command '{$command}'" );
52        }
53
54        $spec = $this->jobObjectSpecs[ $command ];
55        $needsTitle = $this->needsTitle( $command, $spec );
56
57        // TODO: revisit support for old method signature
58        if ( $params instanceof PageReference ) {
59            // Backwards compatibility for old signature ($command, $title, $params)
60            $title = Title::newFromPageReference( $params );
61            $params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
62        } elseif ( isset( $params['namespace'] ) && isset( $params['title'] ) ) {
63            // Handle job classes that take title as constructor parameter.
64            // If a newer classes like GenericParameterJob uses these parameters,
65            // then this happens in Job::__construct instead.
66            $title = Title::makeTitle(
67                $params['namespace'],
68                $params['title']
69            );
70        } else {
71            // Default title for job classes not implementing GenericParameterJob.
72            // This must be a valid title because it not directly passed to
73            // our Job constructor, but rather its subclasses which may expect
74            // to be able to use it.
75            $title = Title::makeTitle(
76                NS_SPECIAL,
77                'Blankpage'
78            );
79        }
80
81        if ( $needsTitle ) {
82            $args = [ $title, $params ];
83        } else {
84            $args = [ $params ];
85        }
86
87        /** @var Job $job */
88        $job = $this->objectFactory->createObject(
89            $spec,
90            [
91                'allowClassName' => true,
92                'allowCallable' => true,
93                'extraArgs' => $args,
94                'assertClass' => Job::class
95            ]
96        );
97
98        // TODO: create a setter, marked @internal
99        $job->command = $command;
100        return $job;
101    }
102
103    /**
104     * Determines whether the job class needs a Title to be passed
105     * as the first parameter to the constructor.
106     *
107     * @param string $command
108     * @param string|array|Closure $spec
109     *
110     * @return bool
111     */
112    private function needsTitle( string $command, $spec ): bool {
113        if ( is_callable( $spec ) ) {
114            $needsTitle = true;
115        } elseif ( is_array( $spec ) ) {
116            if ( isset( $spec['needsPage'] ) ) {
117                $needsTitle = $spec['needsPage'];
118            } elseif ( isset( $spec['class'] ) ) {
119                $needsTitle = !is_subclass_of( $spec['class'],
120                    GenericParameterJob::class );
121            } elseif ( isset( $spec['factory'] ) ) {
122                $needsTitle = true;
123            } else {
124                throw new InvalidArgumentException(
125                    "Invalid job specification for '{$command}': " .
126                    "must contain the 'class' or 'factory' key."
127                );
128            }
129        } elseif ( is_string( $spec ) ) {
130            $needsTitle = !is_subclass_of( $spec,
131                GenericParameterJob::class );
132        } else {
133            throw new InvalidArgumentException(
134                "Invalid job specification for '{$command}': " .
135                "must be a callable, an object spec array, or a class name"
136            );
137        }
138
139        return $needsTitle;
140    }
141}