TasksQueue   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 98.88%

Importance

Changes 0
Metric Value
wmc 32
lcom 1
cbo 12
dl 0
loc 277
ccs 88
cts 89
cp 0.9888
rs 9.6
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A setParallel() 0 4 1
A execute() 0 12 2
A on() 0 4 1
B run() 0 22 4
B buildPipeline() 0 25 3
C executeJob() 0 32 7
A runSynchronously() 0 17 3
A runAsynchronously() 0 18 4
B getStages() 0 20 6
A isValidStage() 0 4 1
1
<?php
2
3
/*
4
 * This file is part of Rocketeer
5
 *
6
 * (c) Maxime Fabre <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 */
12
13
namespace Rocketeer\Services\Tasks;
14
15
use Closure;
16
use Exception;
17
use KzykHys\Parallel\Parallel;
18
use LogicException;
19
use Rocketeer\Services\Connections\Connections\ConnectionInterface;
20
use Rocketeer\Traits\ContainerAwareTrait;
21
use Rocketeer\Traits\Properties\HasHistoryTrait;
22
23
/**
24
 * Handles running an array of tasks sequentially
25
 * or in parallel.
26
 */
27
class TasksQueue
28
{
29
    use ContainerAwareTrait;
30
    use HasHistoryTrait;
31
32
    /**
33
     * @var Parallel
34
     */
35
    protected $parallel;
36
37
    /**
38
     * A list of Tasks to execute.
39
     *
40
     * @var array
41
     */
42
    protected $tasks;
43
44
    /**
45
     * @param Parallel $parallel
46
     */
47 2
    public function setParallel($parallel)
48
    {
49 2
        $this->parallel = $parallel;
50 2
    }
51
52
    ////////////////////////////////////////////////////////////////////
53
    ////////////////////////////// SHORTCUTS ///////////////////////////
54
    ////////////////////////////////////////////////////////////////////
55
56
    /**
57
     * Execute Tasks on the default connection and
58
     * return their output.
59
     *
60
     * @param string|array|Closure $queue
61
     * @param string|string[]|null $connections
62
     *
63
     * @return string|string[]|false
64
     */
65 9
    public function execute($queue, $connections = null)
66
    {
67 9
        if ($connections) {
68 4
            $this->connections->setActiveConnections($connections);
69 4
        }
70
71
        // Run tasks
72 9
        $this->run($queue);
73 9
        $history = $this->history->getFlattenedOutput();
74
75 9
        return end($history);
76
    }
77
78
    /**
79
     * Execute Tasks on various connections.
80
     *
81
     * @param string|string[]      $connections
82
     * @param string|array|Closure $queue
83
     *
84
     * @return string|string[]|false
85
     */
86 2
    public function on($connections, $queue)
87
    {
88 2
        return $this->execute($queue, $connections);
89
    }
90
91
    ////////////////////////////////////////////////////////////////////
92
    //////////////////////////////// QUEUE /////////////////////////////
93
    ////////////////////////////////////////////////////////////////////
94
95
    /**
96
     * Run the queue
97
     * Run an array of Tasks instances on the various
98
     * connections and stages provided.
99
     *
100
     * @param string|array $tasks An array of tasks
101
     *
102
     * @throws Exception
103
     *
104
     * @return Pipeline
105
     */
106 23
    public function run($tasks)
107
    {
108 20
        $tasks = is_array($tasks) ? $tasks : [$tasks];
109 20
        $queue = $this->builder->buildTasks($tasks);
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method buildTasks does not exist on object<Rocketeer\Services\Builders\Builder>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
110 23
        $pipeline = $this->buildPipeline($queue);
111
112
        // Wrap job in closure pipeline
113 20
        foreach ($pipeline as $key => $job) {
114 19
            $pipeline[$key] = function () use ($job) {
115 19
                return $this->executeJob($job);
116
            };
117 20
        }
118
119
        // Run the tasks and store the results
120 20
        if ($this->getOption('parallel')) {
121 2
            $pipeline = $this->runAsynchronously($pipeline);
122 2
        } else {
123 18
            $pipeline = $this->runSynchronously($pipeline);
124
        }
125
126 20
        return $pipeline;
127
    }
128
129
    /**
130
     * Build a pipeline of jobs for Parallel to execute.
131
     *
132
     * @param array $queue
133
     *
134
     * @return Pipeline
135
     */
136 21
    public function buildPipeline(array $queue)
137
    {
138
        // First we'll build the queue
139 21
        $pipeline = new Pipeline();
140
141
        // Get the connections to execute the tasks on
142
        /** @var ConnectionInterface[] $connections */
143 21
        $connections = $this->connections->getActiveConnections();
144 21
        foreach ($connections as $connection) {
145 21
            $connectionKey = $connection->getConnectionKey();
146 21
            $stages = $this->getStages($connectionKey);
147
148
            // Add job to pipeline
149 21
            foreach ($stages as $stage) {
150 21
                $connectionKey->stage = $stage;
151
152 21
                $pipeline[] = new Job([
153 21
                    'connectionKey' => clone $connectionKey,
154 21
                    'queue' => $queue,
155 21
                ]);
156 21
            }
157 21
        }
158
159 21
        return $pipeline;
160
    }
161
162
    /**
163
     * Run the queue, taking into account the stage.
164
     *
165
     * @param Job $job
166
     *
167
     * @return bool
168
     */
169 23
    public function executeJob(Job $job)
170
    {
171
        // Set proper server
172 19
        $connectionKey = $job->connectionKey;
173 19
        $this->connections->setCurrentConnection($connectionKey);
174
175 19
        foreach ($job->queue as $key => $task) {
176 19
            if ($task->usesStages()) {
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method usesStages does not exist on object<Rocketeer\Tasks\AbstractTask>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
177 3
                $stage = $task->usesStages() ? $connectionKey->stage : null;
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method usesStages does not exist on object<Rocketeer\Tasks\AbstractTask>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
178 3
                $this->connections->setStage($stage);
179 3
            }
180
181
            // Check if the current server can run the task
182 19
            if (!$this->connections->getCurrentConnection()->isCompatibleWith($task)) {
183 1
                continue;
184
            }
185
186
            // Here we fire the task, save its
187
            // output and return its status
188 19
            $state = $task->fire();
189 19
            $this->toOutput($state);
190
191
            // If the task didn't finish, display what the error was
192 19
            if ($task->wasHalted() || $state === false) {
193 5
                $this->explainer->error('The tasks queue was canceled by task "'.$task->getName().'"');
194
195 5
                return false;
196
            }
197 17
        }
198
199 23
        return true;
200
    }
201
202
    //////////////////////////////////////////////////////////////////////
203
    ////////////////////////////// RUNNERS ///////////////////////////////
204
    //////////////////////////////////////////////////////////////////////
205
206
    /**
207
     * Run the pipeline in order.
208
     * As long as the previous entry didn't fail, continue.
209
     *
210
     * @param Pipeline $pipeline
211
     *
212
     * @return Pipeline
213
     */
214 19
    protected function runSynchronously(Pipeline $pipeline)
215
    {
216 19
        $results = [];
217
218
        /** @var Closure $task */
219 19
        foreach ($pipeline as $key => $task) {
220 19
            $results[$key] = $this->bash->checkResults($task());
0 ignored issues
show
Documentation Bug introduced by Maxime Fabre
The method checkResults does not exist on object<Rocketeer\Services\Connections\Shell\Bash>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
221 19
            if (!$results[$key]) {
222 5
                break;
223
            }
224 19
        }
225
226
        // Update Pipeline results
227 19
        $pipeline->setResults($results);
228
229 19
        return $pipeline;
230
    }
231
232
    /**
233
     * Run the pipeline in parallel order.
234
     *
235
     * @param Pipeline $pipeline
236
     *
237
     * @throws \Exception
238
     *
239
     * @return Pipeline
240
     */
241 2
    protected function runAsynchronously(Pipeline $pipeline)
242
    {
243 2
        $this->parallel = $this->parallel ?: new Parallel();
244
245
        // Check if supported
246 2
        if (!$this->parallel->isSupported()) {
247
            throw new Exception('Parallel jobs require the PCNTL extension');
248
        }
249
250
        try {
251 2
            $results = $this->parallel->values($pipeline->all());
252 1
            $pipeline->setResults($results);
253 2
        } catch (LogicException $exception) {
254 1
            return $this->runSynchronously($pipeline);
255
        }
256
257 1
        return $pipeline;
258
    }
259
260
    ////////////////////////////////////////////////////////////////////
261
    //////////////////////////////// STAGES ////////////////////////////
262
    ////////////////////////////////////////////////////////////////////
263
264
    /**
265
     * Get the stages of a connection.
266
     *
267
     * @param string $connection
268
     *
269
     * @return array
270
     */
271 23
    public function getStages($connection)
272
    {
273 23
        $this->connections->setCurrentConnection($connection);
274
275 23
        $stage = $this->config->getContextually('stages.default');
0 ignored issues
show
Bug introduced by Maxime Fabre
It seems like you code against a concrete implementation and not the interface Rocketeer\Services\Config\ConfigurationInterface as the method getContextually() does only exist in the following implementations of said interface: Rocketeer\Services\Config\ContextualConfiguration.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
276 23
        if ($this->hasCommand()) {
277 23
            $stage = $this->getOption('stage') ?: $stage;
278 23
        }
279
280
        // Return all stages if "all"
281 23
        if ($stage === 'all' || !$stage) {
282 22
            $stage = $this->connections->getAvailableStages();
283 22
        }
284
285
        // Sanitize and filter
286 23
        $stages = (array) $stage;
287 23
        $stages = array_filter($stages, [$this, 'isValidStage']);
288
289 23
        return $stages ?: [null];
290
    }
291
292
    /**
293
     * Check if a stage is valid.
294
     *
295
     * @param string $stage
296
     *
297
     * @return bool
298
     */
299 5
    public function isValidStage($stage)
300
    {
301 5
        return in_array($stage, $this->connections->getAvailableStages(), true);
302
    }
303
}
304