Passed
Pull Request — master (#4)
by
unknown
07:05
created

SyncTimings::retrieveSentTimeEntries()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
cc 4
nc 8
nop 0
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 array $config
182
     * @param string $entryKey
183
     * @param bool $hasAlreadyBeenSent
184
     *
185
     * @return bool
186
     */
187
    private function timeEntryCanBeLoggedByConfig(array $config, string $entryKey, bool $hasAlreadyBeenSent): bool
188
    {
189
        if ($hasAlreadyBeenSent) {
190
            return false;
191
        }
192
193
        return (is_array($config) && array_key_exists($entryKey, $config));
194
    }
195
196
    /**
197
     * @param TimeEntry $entry
198
     * @param array $clients
199
     * @param string $clientKey
200
     *
201
     * @return void
202
     */
203
    private function logTask(TimeEntry $entry, array $clients = NULL, string $clientKey = NULL, array $projects = NULL, string $projectKey = NULL)
204
    {
205
        $task = new Task();
206
207
        $task->setDescription($this->buildTaskDescription($entry));
208
        $task->setTimeLog($this->buildTimeLog($entry));
209
        if (isset($clients) && isset($clientKey)) {
210
            $task->setClientId($clients[$clientKey]);
211
        }
212
213
        if (isset($projects) && isset($projectKey)) {
214
            $task->setProjectId($projects[$projectKey]);
215
        }
216
217
        $this->invoiceNinjaClient->saveNewTask($task);
218
    }
219
220
    /**
221
     * @param TimeEntry $entry
222
     *
223
     * @return string
224
     */
225
    private function buildTaskDescription(TimeEntry $entry): string
226
    {
227
        $description = '';
228
229
        if ($entry->getProject()) {
230
            $description .= $entry->getProject() . ': ';
231
        }
232
233
        $description .= $entry->getDescription();
234
235
        return $description;
236
    }
237
238
    /**
239
     * @param TimeEntry $entry
240
     *
241
     * @return string
242
     */
243
    private function buildTimeLog(TimeEntry $entry): string
244
    {
245
        $timeLog = [[
246
            $entry->getStart()->getTimestamp(),
247
            $entry->getEnd()->getTimestamp(),
248
        ]];
249
250
        return \GuzzleHttp\json_encode($timeLog);
251
    }
252
253
    /**
254
     * Retrieve clients from Toggl and match them with their corresponding InvoiceNinja client,
255
     * or create a new InvoiceNinja client for the Toggl client.
256
     * 
257
     * @param  int    $workspaceId
258
     * @param  array  $workspaceClients
259
     *
260
     * @return array
261
     */
262
    private function retrieveClientsForWorkspace(int $workspaceId, array $workspaceClients): array
263
    {
264
        $togglClients = $this->togglClient->getClientsForWorkspace($workspaceId);
265
        $invoiceNinjaClients = $this->invoiceNinjaClient->getClients();
266
267
        $clients = [];
268
269
        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...
270
            if (!isset($workspaceClients[$togglClient->getName()])) {
271
                $found = false;
272
                foreach ($invoiceNinjaClients as $invoiceNinjaClient) {
273
                    if ($invoiceNinjaClient->getIsDeleted() == false && strcasecmp($togglClient->getName(), $invoiceNinjaClient->getName()) == 0) {
274
                        $clients[$invoiceNinjaClient->getName()] = $invoiceNinjaClient->getId();
275
                        $found = true;
276
                    }
277
                }
278
                if (!$found) {
279
                    $client = new InvoiceNinjaClientDto();
280
281
                    $client->setName($togglClient->getName());
282
283
                    $clients[$togglClient->getName()] = $this->invoiceNinjaClient->saveNewClient($client)->getId();
284
285
                    $this->io->success('Client ('. $togglClient->getName() . ') created in InvoiceNinja');
286
                }
287
            }
288
        }
289
290
        return $clients;
291
    }
292
293
    /**
294
     * Retrieve projects from Toggl and match them with their corresponding InvoiceNinja project,
295
     * or create a new InvoiceNinja project for the Toggl project.
296
     * 
297
     * @param  int    $workspaceId
298
     * @param  array  $workspaceClients
299
     * @param  array  $workspaceProjects
300
     * 
301
     * @return array
302
     */
303
    private function retrieveProjectsForWorkspace(int $workspaceId, array &$workspaceClients, array $workspaceProjects): array
304
    {
305
        $togglProjects = $this->togglClient->getProjectsForWorkspace($workspaceId);
306
        $togglClients = $this->togglClient->getClientsForWorkspace($workspaceId);
307
        $invoiceNinjaProjects = $this->invoiceNinjaClient->getProjects();
308
309
        $projects = [];
310
311
        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...
312
            if (!isset($workspaceProjects[$togglProject->getName()])) {
313
                $found = false;
314
                foreach ($invoiceNinjaProjects as $invoiceNinjaProject) {
315
                    if ($invoiceNinjaProject->getIsDeleted() == false && strcasecmp($togglProject->getName(), $invoiceNinjaProject->getName()) == 0) {
316
                        $projects[$invoiceNinjaProject->getName()] = $invoiceNinjaProject->getId();
317
                        $found = true;
318
                    }
319
                }
320
                if (!$found) {
321
                    $clientPresent = true;
322
323
                    $project = new InvoiceNinjaProject();
324
325
                    $project->setName($togglProject->getName());
326
327
                    if ($togglProject->getCid() !== null) {
328
                        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...
329
                            if ($togglClient->getWid() == $workspaceId && $togglClient->getId() == $togglProject->getCid()) {
330
                                $project->setClientId($workspaceClients[$togglClient->getName()]);
331
                            }
332
                        }
333
                    } else if ($this->useProjectsAsClients) {
334
                        $client = new InvoiceNinjaClientDto();
335
336
                        $client->setName($togglProject->getName());
337
338
                        $workspaceClients[$togglProject->getName()] = $this->invoiceNinjaClient->saveNewClient($client)->getId();
339
340
                        $this->io->success('Project ('. $togglProject->getName() . ') created as Client in InvoiceNinja');
341
342
                        $project->setClientId($workspaceClients[$togglProject->getName()]);
343
                    } else {
344
                        $this->io->error('Client not provided for Project ('. $togglProject->getName() . ') in Toggl');
345
                        $this->io->warning("To allow using projects as clients enable 'use_projects_as_clients' in parameters config file");
346
                    }
347
348
                    if ($clientPresent) {
349
                        $projects[$togglProject->getName()] = $this->invoiceNinjaClient->saveNewProject($project)->getId();
350
351
                        $this->io->success('Project ('. $togglProject->getName() . ') created in InvoiceNinja');
352
                    }
353
                }
354
            }
355
        }
356
357
        return $projects;
358
    }
359
360
    /**
361
     * Retrieve the Invoice Ninja client id given the name of a project the client is assigned to.
362
     * @param  string $projectName
363
     * @return string
364
     */
365
    private function getInvoiceNinjaClientIdForProject(string $projectName): ?string
366
    {
367
        $invoiceNinjaProjects  = $this->invoiceNinjaClient->getProjects();
368
        $invoiceNinjaClientId = null;
369
370
        foreach ($invoiceNinjaProjects as $invoiceNinjaProject) {
371
            if ($invoiceNinjaProject->getName() == $projectName) {
372
                $invoiceNinjaClientId = $invoiceNinjaProject->getClientId();
373
            }
374
        }
375
376
        return $invoiceNinjaClientId;
377
    }
378
379
    /**
380
     * Retrieve log of past sent time entries to prevent sending the same time entries over again.
381
     * 
382
     * @return array
383
     */
384
    private function retrieveSentTimeEntries(): array
385
    {
386
        if (!file_exists($this->storageDir)) {
387
            mkdir($this->storageDir, 0777, true);
388
        }
389
390
        if (!file_exists($this->storageDir . $this->storageFileName)) {
391
            touch($this->storageDir . $this->storageFileName);
392
        }
393
        
394
        $this->sentTimeEntries = unserialize(file_get_contents($this->storageDir . $this->storageFileName));
395
396
        if (!is_array($this->sentTimeEntries)) {
397
            $this->sentTimeEntries = [];
398
        }
399
400
        return $this->sentTimeEntries;
401
    }
402
403
    /**
404
     * Store newly sent time entries into time entry log.
405
     *
406
     * @return void
407
     */
408
    private function storeSentTimeEntries()
409
    {
410
        file_put_contents($this->storageDir . $this->storageFileName, serialize($this->sentTimeEntries));
411
    }
412
}
413