This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more
1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Loevgaard\DandomainConsignmentBundle\ConsignmentService; |
||
6 | |||
7 | use Doctrine\Common\Persistence\ManagerRegistry; |
||
8 | use Doctrine\ORM\EntityManager; |
||
9 | use Doctrine\ORM\OptimisticLockException; |
||
10 | use Doctrine\ORM\ORMException; |
||
11 | use Doctrine\ORM\QueryBuilder; |
||
12 | use Loevgaard\DandomainConsignment\Entity\Generated\ReportInterface; |
||
13 | use Loevgaard\DandomainConsignment\Entity\Report; |
||
14 | use Loevgaard\DandomainConsignment\Repository\ReportRepository; |
||
15 | use Loevgaard\DandomainConsignmentBundle\Event\ReportNotDeliveredEvent; |
||
16 | use Loevgaard\DandomainConsignmentBundle\Exception\InvalidBarCodeException; |
||
17 | use Loevgaard\DandomainConsignmentBundle\Exception\InvalidVendorNumberException; |
||
18 | use Loevgaard\DandomainFoundation\Entity\Generated\ManufacturerInterface; |
||
19 | use Loevgaard\DandomainStock\Entity\Generated\StockMovementInterface; |
||
20 | use Loevgaard\DandomainStock\Entity\StockMovement; |
||
21 | use Psr\Log\LoggerInterface; |
||
22 | use Psr\Log\NullLogger; |
||
23 | use Symfony\Component\EventDispatcher\EventDispatcherInterface; |
||
24 | use Symfony\Component\OptionsResolver\OptionsResolver; |
||
25 | |||
26 | abstract class ConsignmentService implements ConsignmentServiceInterface |
||
27 | { |
||
28 | /** |
||
29 | * @var EntityManager |
||
30 | */ |
||
31 | protected $entityManager; |
||
32 | |||
33 | /** |
||
34 | * @var ReportRepository |
||
35 | */ |
||
36 | protected $reportRepository; |
||
37 | |||
38 | /** |
||
39 | * @var EventDispatcherInterface |
||
40 | */ |
||
41 | protected $eventDispatcher; |
||
42 | |||
43 | /** |
||
44 | * The directory where report files will be saved. |
||
45 | * |
||
46 | * @var string |
||
47 | */ |
||
48 | protected $reportDir; |
||
49 | |||
50 | /** |
||
51 | * @var LoggerInterface |
||
52 | */ |
||
53 | protected $logger; |
||
54 | |||
55 | /** |
||
56 | * @var ManufacturerInterface |
||
57 | */ |
||
58 | protected $manufacturer; |
||
59 | |||
60 | /** |
||
61 | * Contains the included product ids. |
||
62 | * |
||
63 | * @var array |
||
64 | */ |
||
65 | protected $includedProductIds; |
||
66 | |||
67 | /** |
||
68 | * Contains the excluded product ids. |
||
69 | * |
||
70 | * @var array |
||
71 | */ |
||
72 | protected $excludedProductIds; |
||
73 | |||
74 | public function __construct(ManagerRegistry $managerRegistry, ReportRepository $reportRepository, EventDispatcherInterface $eventDispatcher, string $reportDir) |
||
75 | { |
||
76 | $this->entityManager = $managerRegistry->getManager(); |
||
77 | $this->reportRepository = $reportRepository; |
||
78 | $this->eventDispatcher = $eventDispatcher; |
||
79 | $this->reportDir = rtrim($reportDir, '/'); |
||
80 | $this->logger = new NullLogger(); |
||
81 | |||
82 | if (!is_dir($this->reportDir)) { |
||
83 | throw new \InvalidArgumentException('The report dir given is not a directory'); |
||
84 | } |
||
85 | |||
86 | if (!is_writable($this->reportDir)) { |
||
87 | throw new \InvalidArgumentException('The report dir given is not writable'); |
||
88 | } |
||
89 | } |
||
90 | |||
91 | /** |
||
92 | * @param array $options |
||
93 | * |
||
94 | * @return ReportInterface |
||
95 | * |
||
96 | * @throws ORMException |
||
97 | */ |
||
98 | public function generateReport(array $options = []): ReportInterface |
||
99 | { |
||
100 | // resolve options |
||
101 | $resolver = new OptionsResolver(); |
||
102 | $this->configureOptions($resolver); |
||
103 | $options = $resolver->resolve($options); |
||
104 | |||
105 | $report = new Report(); |
||
106 | $report->setManufacturer($this->manufacturer); |
||
107 | |||
108 | $this->reportRepository->persist($report); |
||
109 | |||
110 | try { |
||
111 | if ($options['valid_bar_codes']) { |
||
112 | $this->validateBarCodes(); |
||
113 | } |
||
114 | |||
115 | if ($options['valid_vendor_numbers']) { |
||
116 | $this->validateVendorNumbers(); |
||
117 | } |
||
118 | |||
119 | $qb = $this->queryBuilder($options); |
||
120 | |||
121 | /** @var StockMovementInterface[] $stockMovements */ |
||
122 | $stockMovements = $qb->getQuery()->getResult(); |
||
123 | |||
124 | if (!count($stockMovements)) { |
||
125 | throw new \Exception('No stock movements applicable for this report'); |
||
126 | } |
||
127 | |||
128 | $lastStockMovement = null; |
||
129 | foreach ($stockMovements as $stockMovement) { |
||
130 | $report->addStockMovement($stockMovement); |
||
131 | |||
132 | $lastStockMovement = $stockMovement; |
||
133 | } |
||
134 | |||
135 | if ($lastStockMovement && $options['update_last_stock_movement']) { |
||
136 | $this->manufacturer->setConsignmentLastStockMovement($lastStockMovement); |
||
137 | } |
||
138 | |||
139 | $report->markAsSuccess(); |
||
140 | } catch (\Exception $e) { |
||
141 | $report->markAsError($e->getMessage()); |
||
142 | } |
||
143 | |||
144 | $this->reportRepository->flush(); |
||
145 | |||
146 | return $report; |
||
147 | } |
||
148 | |||
149 | /** |
||
150 | * @param ReportInterface $report |
||
151 | * @param array $options |
||
152 | * |
||
153 | * @return \SplFileObject |
||
154 | * |
||
155 | * @throws ORMException |
||
156 | * @throws OptimisticLockException |
||
157 | */ |
||
158 | public function generateReportFile(ReportInterface $report, array $options = []): \SplFileObject |
||
159 | { |
||
160 | $file = $this->getFile(); |
||
161 | |||
162 | foreach ($report->getStockMovements() as $stockMovement) { |
||
163 | $file->fputcsv([ |
||
164 | $stockMovement->getQuantity(), |
||
165 | $stockMovement->getProduct()->getBarCodeNumber(), |
||
166 | ]); |
||
167 | } |
||
168 | |||
169 | $report->setFile($file); |
||
170 | |||
171 | $this->reportRepository->flush(); |
||
172 | |||
173 | return $file; |
||
174 | } |
||
175 | |||
176 | /** |
||
177 | * @param ReportInterface $report |
||
178 | * @param array $options |
||
179 | * |
||
180 | * @return bool |
||
181 | * |
||
182 | * @throws ORMException |
||
183 | * @throws OptimisticLockException |
||
184 | */ |
||
185 | public function deliverReport(ReportInterface $report, array $options = []): bool |
||
186 | { |
||
187 | if (!$report->isDeliverable()) { |
||
188 | $this->eventDispatcher->dispatch(ReportNotDeliveredEvent::NAME, new ReportNotDeliveredEvent($report)); |
||
189 | |||
190 | return false; |
||
191 | } |
||
192 | |||
193 | $file = $report->getFile(); |
||
194 | if (!$file || !$file->isFile()) { |
||
195 | $file = $this->generateReportFile($report); |
||
196 | } |
||
197 | |||
198 | $this->logger->info('File has been delivered to: '.$file->getPathname()); |
||
199 | |||
200 | /* |
||
0 ignored issues
–
show
|
|||
201 | Example of mailing the report |
||
202 | ------- |
||
203 | $recipients = ['[email protected]']; |
||
204 | |||
205 | $attachment = \Swift_Attachment::fromPath($file->getPathname()); |
||
206 | $attachment->setFilename('report-'.$report->getId().'.csv'); |
||
207 | |||
208 | $message = \Swift_Message::newInstance() |
||
209 | ->attach($attachment) |
||
210 | ->setSubject('Consignment report (id: '.$report->getId().')') |
||
211 | ->setFrom('[email protected]', 'Your Business') |
||
212 | ->setTo($recipients) |
||
213 | ->setBody('See the attached file.', 'text/plain') |
||
214 | ; |
||
215 | |||
216 | $this->mailer->send($message); |
||
217 | */ |
||
218 | |||
219 | return true; |
||
220 | } |
||
221 | |||
222 | public function queryBuilder(array $options = [], string $alias = 's'): QueryBuilder |
||
223 | { |
||
224 | // resolve options |
||
225 | $resolver = new OptionsResolver(); |
||
226 | $this->configureOptions($resolver); |
||
227 | $options = $resolver->resolve($options); |
||
228 | |||
229 | $includedProductIds = $this->getIncludedProductIds(); |
||
230 | |||
231 | if (!count($includedProductIds)) { |
||
232 | throw new \RuntimeException('No included product ids. Something is wrong'); |
||
233 | } |
||
234 | |||
235 | $qb = $this->entityManager->createQueryBuilder(); |
||
236 | $qb->select('s, p') |
||
237 | ->from('Loevgaard\DandomainStock\Entity\StockMovement', 's') |
||
238 | ->join('s.product', 'p') |
||
239 | ->andWhere($qb->expr()->in('p.id', ':includedProductIds')) |
||
240 | ->andWhere($qb->expr()->in('s.type', ':stockMovementTypes')) |
||
241 | ->addOrderBy('s.id', 'asc') |
||
242 | ->setParameters([ |
||
243 | 'includedProductIds' => $includedProductIds, |
||
244 | 'stockMovementTypes' => $options['stock_movement_types'], |
||
245 | ]); |
||
246 | |||
247 | if (!$options['include_complaints']) { |
||
248 | $qb->andWhere('s.complaint = 0'); |
||
249 | } |
||
250 | |||
251 | if ($options['use_last_stock_movement'] && $this->manufacturer->getConsignmentLastStockMovement()) { |
||
252 | $qb->andWhere($qb->expr()->gt('s.id', ':lastStockMovementId')) |
||
253 | ->setParameter('lastStockMovementId', $this->manufacturer->getConsignmentLastStockMovement()->getId()); |
||
254 | } |
||
255 | |||
256 | if ($options['start_date']) { |
||
257 | $qb->andWhere($qb->expr()->gte('s.createdAt', ':startDate')) |
||
258 | ->setParameter('startDate', $options['start_date']); |
||
259 | } |
||
260 | |||
261 | if ($options['end_date']) { |
||
262 | $qb->andWhere($qb->expr()->lte('s.createdAt', ':endDate')) |
||
263 | ->setParameter('endDate', $options['end_date']); |
||
264 | } |
||
265 | |||
266 | return $qb; |
||
267 | } |
||
268 | |||
269 | public function getProductQueryBuilder(string $alias = 'p'): QueryBuilder |
||
270 | { |
||
271 | $excludedProductIds = $this->getExcludedProductIds(); |
||
272 | |||
273 | $qb = $this->entityManager->createQueryBuilder(); |
||
274 | $qb->select($alias) |
||
275 | ->from('Loevgaard\DandomainFoundation\Entity\Product', $alias) |
||
276 | ->where($qb->expr()->isMemberOf(':manufacturer', $alias.'.manufacturers')) |
||
277 | ->setParameter('manufacturer', $this->manufacturer); |
||
278 | |||
279 | if (!empty($excludedProductIds)) { |
||
280 | $qb->andWhere($qb->expr()->notIn($alias.'.id', ':excluded')) |
||
281 | ->setParameter('excluded', $excludedProductIds); |
||
282 | } |
||
283 | |||
284 | return $qb; |
||
285 | } |
||
286 | |||
287 | /** |
||
288 | * @param LoggerInterface $logger |
||
289 | * |
||
290 | * @return ConsignmentServiceInterface |
||
291 | */ |
||
292 | public function setLogger(LoggerInterface $logger): ConsignmentServiceInterface |
||
293 | { |
||
294 | $this->logger = $logger; |
||
295 | |||
296 | return $this; |
||
297 | } |
||
298 | |||
299 | /** |
||
300 | * @param ManufacturerInterface $manufacturer |
||
301 | * |
||
302 | * @return ConsignmentServiceInterface |
||
303 | */ |
||
304 | public function setManufacturer(ManufacturerInterface $manufacturer): ConsignmentServiceInterface |
||
305 | { |
||
306 | $this->manufacturer = $manufacturer; |
||
307 | |||
308 | return $this; |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * This method should return an array of included product ids |
||
313 | * It excludes the excluded product ids, by using the getProductQueryBuilder method. |
||
314 | * |
||
315 | * @return array |
||
316 | */ |
||
317 | protected function getIncludedProductIds(): array |
||
318 | { |
||
319 | if (!$this->includedProductIds) { |
||
0 ignored issues
–
show
The expression
$this->includedProductIds of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||
320 | $qb = $this->getProductQueryBuilder(); |
||
321 | $qb->select('p.id'); |
||
322 | |||
323 | $res = $qb->getQuery()->getArrayResult(); |
||
324 | |||
325 | $this->includedProductIds = array_map(function ($elm) { |
||
326 | return array_values($elm)[0]; |
||
327 | }, $res); |
||
328 | } |
||
329 | |||
330 | return $this->includedProductIds; |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * This method should return an array of excluded product ids. |
||
335 | * |
||
336 | * @return array |
||
337 | */ |
||
338 | protected function getExcludedProductIds(): array |
||
339 | { |
||
340 | return []; |
||
341 | } |
||
342 | |||
343 | /** |
||
344 | * This is a helper method which takes an array of product numbers and returns their respective product ids. |
||
345 | * |
||
346 | * @param array $numbers |
||
347 | * |
||
348 | * @return array |
||
349 | */ |
||
350 | protected function getProductIdsFromProductNumbers(array $numbers): array |
||
351 | { |
||
352 | $qb = $this->entityManager->createQueryBuilder(); |
||
353 | $qb->select('p.id') |
||
354 | ->from('Loevgaard\DandomainFoundation\Entity\Product', 'p') |
||
355 | ->where($qb->expr()->in('p.number', ':numbers')) |
||
356 | ->setParameter('numbers', $numbers); |
||
357 | |||
358 | $res = $qb->getQuery()->getArrayResult(); |
||
359 | |||
360 | return array_map(function ($elm) { |
||
361 | return array_values($elm)[0]; |
||
362 | }, $res); |
||
363 | } |
||
364 | |||
365 | /** |
||
366 | * @throws InvalidBarCodeException |
||
367 | */ |
||
368 | protected function validateBarCodes(): void |
||
369 | { |
||
370 | $qb = $this->queryBuilder(); |
||
371 | $qb->andWhere('p.validBarCode = 0'); |
||
372 | |||
373 | /** @var StockMovementInterface[] $stockMovements */ |
||
374 | $stockMovements = $qb->getQuery()->getResult(); |
||
375 | |||
376 | $c = count($stockMovements); |
||
377 | |||
378 | View Code Duplication | if ($c) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
379 | $this->logger->emergency('There are '.$c.' stock movements with invalid bar codes'); |
||
380 | $productNumbers = []; |
||
381 | foreach ($stockMovements as $stockMovement) { |
||
382 | $productNumbers[] = $stockMovement->getProduct()->getNumber(); |
||
383 | } |
||
384 | |||
385 | throw new InvalidBarCodeException('Products with invalid bar codes: '.join(', ', $productNumbers), $productNumbers); |
||
386 | } |
||
387 | } |
||
388 | |||
389 | /** |
||
390 | * @throws InvalidVendorNumberException |
||
391 | */ |
||
392 | protected function validateVendorNumbers(): void |
||
393 | { |
||
394 | $qb = $this->queryBuilder(); |
||
395 | $qb->andWhere($qb->expr()->orX( |
||
396 | $qb->expr()->eq('p.vendorNumber', ':empty'), |
||
397 | $qb->expr()->isNull('p.vendorNumber') |
||
398 | ))->setParameter(':empty', ''); |
||
399 | |||
400 | /** @var StockMovementInterface[] $stockMovements */ |
||
401 | $stockMovements = $qb->getQuery()->getResult(); |
||
402 | |||
403 | $c = count($stockMovements); |
||
404 | |||
405 | View Code Duplication | if ($c) { |
|
0 ignored issues
–
show
This code seems to be duplicated across your project.
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation. You can also find more detailed suggestions in the “Code” section of your repository. ![]() |
|||
406 | $this->logger->critical('There are '.$c.' stock movements with invalid vendor numbers'); |
||
407 | $productNumbers = []; |
||
408 | foreach ($stockMovements as $stockMovement) { |
||
409 | $productNumbers[] = $stockMovement->getProduct()->getNumber(); |
||
410 | } |
||
411 | |||
412 | throw new InvalidVendorNumberException('Products with invalid vendor numbers: '.join($productNumbers), $productNumbers); |
||
413 | } |
||
414 | } |
||
415 | |||
416 | protected function getFile(string $extension = 'csv'): \SplFileObject |
||
417 | { |
||
418 | do { |
||
419 | $filename = $this->reportDir.'/'.uniqid('consignment-', true).'.'.$extension; |
||
420 | } while (file_exists($filename)); |
||
421 | |||
422 | return new \SplFileObject($filename, 'w+'); |
||
423 | } |
||
424 | |||
425 | protected function configureOptions(OptionsResolver $resolver): void |
||
426 | { |
||
427 | $resolver->setDefaults([ |
||
428 | 'valid_bar_codes' => false, |
||
429 | 'valid_vendor_numbers' => false, |
||
430 | 'update_last_stock_movement' => true, |
||
431 | 'stock_movement_types' => [ |
||
432 | StockMovement::TYPE_RETURN, |
||
433 | StockMovement::TYPE_SALE, |
||
434 | StockMovement::TYPE_REGULATION, |
||
435 | ], |
||
436 | 'include_complaints' => false, |
||
437 | 'use_last_stock_movement' => true, |
||
438 | 'start_date' => null, |
||
439 | 'end_date' => null, |
||
440 | ]); |
||
441 | } |
||
442 | } |
||
443 |
Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.
The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.
This check looks for comments that seem to be mostly valid code and reports them.