Passed
Pull Request — master (#4)
by
unknown
01:53
created

SyncTimings::retrieveClientsForWorkspace()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 30
rs 8.5066
c 0
b 0
f 0
cc 7
nc 8
nop 2
1
<?php declare(strict_types=1);
2
3
namespace Syncer\Command;
4
5
use Syncer\Dto\InvoiceNinja\Task;
6
use Syncer\Dto\Toggl\TimeEntry;
7
use Syncer\Dto\InvoiceNinja\Client as InvoiceNinjaClientDto;
8
use Syncer\Dto\InvoiceNinja\Project as InvoiceNinjaProject;
9
use Syncer\InvoiceNinja\InvoiceNinjaClient;
10
use Syncer\Toggl\ReportsClient;
11
use Syncer\Toggl\TogglClient;
12
13
use Symfony\Component\Console\Command\Command;
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Output\OutputInterface;
16
use Symfony\Component\Console\Style\SymfonyStyle;
17
18
/**
19
 * Class SyncTimings
20
 * @package Syncer\Command
21
 *
22
 * @author Matthieu Calie <[email protected]>
23
 */
24
class SyncTimings extends Command
25
{
26
    /**
27
     * @var SymfonyStyle
28
     */
29
    private $io;
30
31
    /**
32
     * @var TogglClient
33
     */
34
    private $togglClient;
35
36
    /**
37
     * @var ReportsClient
38
     */
39
    private $reportsClient;
40
41
    /**
42
     * @var InvoiceNinjaClient
43
     */
44
    private $invoiceNinjaClient;
45
46
    /**
47
     * @var array
48
     */
49
    private $clients;
50
51
    /**
52
     * @var array
53
     */
54
    private $projects;
55
56
    /**
57
     * @var string
58
     */
59
    private $storageDir;
60
61
    /**
62
     * @var string
63
     */
64
    private $storageFileName;
65
66
    /**
67
     * @var bool
68
     */
69
    private $useProjectsAsClients;
70
71
    /**
72
     * SyncTimings constructor.
73
     *
74
     * @param TogglClient $togglClient
75
     * @param ReportsClient $reportsClient
76
     * @param InvoiceNinjaClient $invoiceNinjaClient
77
     * @param array $clients
78
     * @param array $projects
79
     */
80
    public function __construct(
81
        TogglClient $togglClient,
82
        ReportsClient $reportsClient,
83
        InvoiceNinjaClient $invoiceNinjaClient,
84
        $clients,
85
        $projects,
86
        String $storageDir,
87
        String $storageFileName,
88
        bool $useProjectsAsClients
89
    ) {
90
        $this->togglClient = $togglClient;
91
        $this->reportsClient = $reportsClient;
92
        $this->invoiceNinjaClient = $invoiceNinjaClient;
93
        $this->clients = $clients;
94
        $this->projects = $projects;
95
        $this->storageDir = $storageDir;
96
        $this->storageFileName = $storageFileName;
97
        $this->useProjectsAsClients = $useProjectsAsClients;
98
        $this->retrieveSentTimeEntries();
99
100
        parent::__construct();
101
    }
102
103
    /**
104
     * Configure the command
105
     */
106
    protected function configure()
107
    {
108
        $this
109
            ->setName('sync:timings')
110
            ->setDescription('Syncs timings from toggl to invoiceninja')
111
        ;
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117
    protected function execute(InputInterface $input, OutputInterface $output)
118
    {
119
        $this->io = new SymfonyStyle($input, $output);
120
        $workspaces = $this->togglClient->getWorkspaces();
121
122
        if (!is_array($workspaces) || count($workspaces) === 0) {
123
            $this->io->error('No workspaces to sync.');
124
125
            return;
126
        }
127
128
        foreach ($workspaces as $workspace) {
129
            $detailedReport = $this->reportsClient->getDetailedReport($workspace->getId());
130
            $workspaceClients = array_merge($this->clients, $this->retrieveClientsForWorkspace($workspace->getId(), $this->clients));
131
            $workspaceProjects = array_merge($this->projects, $this->retrieveProjectsForWorkspace($workspace->getId(), $workspaceClients, $this->projects));
132
133
            foreach($detailedReport->getData() as $timeEntry) {
134
                $timeEntrySent = false;
0 ignored issues
show
Unused Code introduced by
$timeEntrySent is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
135
                $timeEntryClient = $timeEntry->getClient();
136
                $timeEntryProject = $timeEntry->getProject();
137
138
                if (in_array($timeEntry->getId(), $this->sentTimeEntries)) {
0 ignored issues
show
Bug introduced by
The property sentTimeEntries does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
139
                    continue;
140
                }
141
142
                // Since all Toggl time entries require there to be a project before there can be a client,
143
                // a project is required, but a client is not.
144
                if ($timeEntryProject === null) {
145
                    $this->io->warning('No project set for TimeEntry (' . $timeEntry->getDescription() . ')');
146
                }
147
148
                else if ($timeEntryClient === null) {
149
                    if ($this->useProjectsAsClients) {
150
                        $timeEntryClient = $timeEntryProject;
151
                        $workspaceClients[$timeEntryProject] = $this->getInvoiceNinjaClientIdForProject($timeEntryProject);
152
                    } else {
153
                        $timeEntryProject = null;
154
                        $this->io->warning('No client set for TimeEntry (' . $timeEntry->getDescription() . ')');
155
                        $this->io->warning("To allow using projects as clients enable 'use_projects_as_clients' in parameters config file");
156
                    }
157
                }
158
159
                if ($timeEntryProject !== null) {
160
                    $this->logTask($timeEntry, $workspaceClients, $timeEntryClient, $workspaceProjects, $timeEntryProject);
161
162
                    $this->sentTimeEntries[] = $timeEntry->getId();
163
                    $timeEntrySent = true;
164
                } else {
165
                    $this->logTask($timeEntry);
166
167
                    $this->sentTimeEntries[] = $timeEntry->getId();
168
                    $timeEntrySent = true;
169
                }
170
171
                if ($timeEntrySent) {
172
                    $this->io->success('TimeEntry ('. $timeEntry->getDescription() . ') sent to InvoiceNinja');
173
                }
174
            }
175
        }
176
177
        $this->storeSentTimeEntries();
178
    }
179
180
    /**
181
     * @param TimeEntry $entry
182
     * @param array $clients
183
     * @param string $clientKey
184
     *
185
     * @return void
186
     */
187
    private function logTask(TimeEntry $entry, array $clients = NULL, string $clientKey = NULL, array $projects = NULL, string $projectKey = NULL)
188
    {
189
        $task = new Task();
190
191
        $task->setDescription($this->buildTaskDescription($entry));
192
        $task->setTimeLog($this->buildTimeLog($entry));
193
        if (isset($clients) && isset($clientKey)) {
194
            $task->setClientId($clients[$clientKey]);
195
        }
196
197
        if (isset($projects) && isset($projectKey)) {
198
            $task->setProjectId($projects[$projectKey]);
199
        }
200
201
        $this->invoiceNinjaClient->saveNewTask($task);
202
    }
203
204
    /**
205
     * @param TimeEntry $entry
206
     *
207
     * @return string
208
     */
209
    private function buildTaskDescription(TimeEntry $entry): string
210
    {
211
        $description = '';
212
213
        if ($entry->getProject()) {
214
            $description .= $entry->getProject() . ': ';
215
        }
216
217
        $description .= $entry->getDescription();
218
219
        return $description;
220
    }
221
222
    /**
223
     * @param TimeEntry $entry
224
     *
225
     * @return string
226
     */
227
    private function buildTimeLog(TimeEntry $entry): string
228
    {
229
        $timeLog = [[
230
            $entry->getStart()->getTimestamp(),
231
            $entry->getEnd()->getTimestamp(),
232
        ]];
233
234
        return \GuzzleHttp\json_encode($timeLog);
235
    }
236
237
    /**
238
     * Retrieve clients from Toggl and match them with their corresponding InvoiceNinja client,
239
     * or create a new InvoiceNinja client for the Toggl client.
240
     * 
241
     * @param  int    $workspaceId
242
     * @param  array  $workspaceClients
243
     *
244
     * @return array
245
     */
246
    private function retrieveClientsForWorkspace(int $workspaceId, array $workspaceClients): array
247
    {
248
        $togglClients = $this->togglClient->getClientsForWorkspace($workspaceId);
249
        $invoiceNinjaClients = $this->invoiceNinjaClient->getClients();
250
251
        $clients = [];
252
253
        foreach ($togglClients as $togglClient) {
0 ignored issues
show
Bug introduced by
The expression $togglClients of type object|array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
254
            if (!isset($workspaceClients[$togglClient->getName()])) {
255
                $found = false;
256
                foreach ($invoiceNinjaClients as $invoiceNinjaClient) {
257
                    if ($invoiceNinjaClient->getIsDeleted() == false && strcasecmp($togglClient->getName(), $invoiceNinjaClient->getName()) == 0) {
258
                        $clients[$invoiceNinjaClient->getName()] = $invoiceNinjaClient->getId();
259
                        $found = true;
260
                    }
261
                }
262
                if (!$found) {
263
                    $client = new InvoiceNinjaClientDto();
264
265
                    $client->setName($togglClient->getName());
266
267
                    $clients[$togglClient->getName()] = $this->invoiceNinjaClient->saveNewClient($client)->getId();
268
269
                    $this->io->success('Client ('. $togglClient->getName() . ') created in InvoiceNinja');
270
                }
271
            }
272
        }
273
274
        return $clients;
275
    }
276
277
    /**
278
     * Retrieve projects from Toggl and match them with their corresponding InvoiceNinja project,
279
     * or create a new InvoiceNinja project for the Toggl project.
280
     * 
281
     * @param  int    $workspaceId
282
     * @param  array  $workspaceClients
283
     * @param  array  $workspaceProjects
284
     * 
285
     * @return array
286
     */
287
    private function retrieveProjectsForWorkspace(int $workspaceId, array &$workspaceClients, array $workspaceProjects): array
288
    {
289
        $togglProjects = $this->togglClient->getProjectsForWorkspace($workspaceId);
290
        $togglClients = $this->togglClient->getClientsForWorkspace($workspaceId);
291
        $invoiceNinjaProjects = $this->invoiceNinjaClient->getProjects();
292
293
        $projects = [];
294
295
        foreach ($togglProjects as $togglProject) {
0 ignored issues
show
Bug introduced by
The expression $togglProjects of type object|array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
296
            if (!isset($workspaceProjects[$togglProject->getName()])) {
297
                $found = false;
298
                foreach ($invoiceNinjaProjects as $invoiceNinjaProject) {
299
                    if ($invoiceNinjaProject->getIsDeleted() == false && strcasecmp($togglProject->getName(), $invoiceNinjaProject->getName()) == 0) {
300
                        $projects[$invoiceNinjaProject->getName()] = $invoiceNinjaProject->getId();
301
                        $found = true;
302
                    }
303
                }
304
                if (!$found) {
305
                    $clientPresent = true;
306
307
                    $project = new InvoiceNinjaProject();
308
309
                    $project->setName($togglProject->getName());
310
311
                    if ($togglProject->getCid() !== null) {
312
                        foreach ($togglClients as $togglClient) {
0 ignored issues
show
Bug introduced by
The expression $togglClients of type object|array|integer|double|string|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
313
                            if ($togglClient->getWid() == $workspaceId && $togglClient->getId() == $togglProject->getCid()) {
314
                                $project->setClientId($workspaceClients[$togglClient->getName()]);
315
                            }
316
                        }
317
                    } else if ($this->useProjectsAsClients) {
318
                        $client = new InvoiceNinjaClientDto();
319
320
                        $client->setName($togglProject->getName());
321
322
                        $workspaceClients[$togglProject->getName()] = $this->invoiceNinjaClient->saveNewClient($client)->getId();
323
324
                        $this->io->success('Project ('. $togglProject->getName() . ') created as Client in InvoiceNinja');
325
326
                        $project->setClientId($workspaceClients[$togglProject->getName()]);
327
                    } else {
328
                        $this->io->error('Client not provided for Project ('. $togglProject->getName() . ') in Toggl');
329
                        $this->io->warning("To allow using projects as clients enable 'use_projects_as_clients' in parameters config file");
330
                    }
331
332
                    if ($clientPresent) {
333
                        $projects[$togglProject->getName()] = $this->invoiceNinjaClient->saveNewProject($project)->getId();
334
335
                        $this->io->success('Project ('. $togglProject->getName() . ') created in InvoiceNinja');
336
                    }
337
                }
338
            }
339
        }
340
341
        return $projects;
342
    }
343
344
    /**
345
     * Retrieve the Invoice Ninja client id given the name of a project the client is assigned to.
346
     * @param  string $projectName
347
     * @return string
348
     */
349
    private function getInvoiceNinjaClientIdForProject(string $projectName): ?string
350
    {
351
        $invoiceNinjaProjects  = $this->invoiceNinjaClient->getProjects();
352
        $invoiceNinjaClientId = null;
353
354
        foreach ($invoiceNinjaProjects as $invoiceNinjaProject) {
355
            if ($invoiceNinjaProject->getName() == $projectName) {
356
                $invoiceNinjaClientId = $invoiceNinjaProject->getClientId();
357
            }
358
        }
359
360
        return $invoiceNinjaClientId;
361
    }
362
363
    /**
364
     * Retrieve log of past sent time entries to prevent sending the same time entries over again.
365
     * 
366
     * @return array
367
     */
368
    private function retrieveSentTimeEntries(): array
369
    {
370
        if (!file_exists($this->storageDir)) {
371
            mkdir($this->storageDir, 0777, true);
372
        }
373
374
        if (!file_exists($this->storageDir . $this->storageFileName)) {
375
            touch($this->storageDir . $this->storageFileName);
376
        }
377
        
378
        $this->sentTimeEntries = unserialize(file_get_contents($this->storageDir . $this->storageFileName));
379
380
        if (!is_array($this->sentTimeEntries)) {
381
            $this->sentTimeEntries = [];
382
        }
383
384
        return $this->sentTimeEntries;
385
    }
386
387
    /**
388
     * Store newly sent time entries into time entry log.
389
     *
390
     * @return void
391
     */
392
    private function storeSentTimeEntries()
393
    {
394
        file_put_contents($this->storageDir . $this->storageFileName, serialize($this->sentTimeEntries));
395
    }
396
}
397