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 | /* |
||
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
|
|||
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) { |
|
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) { |
|
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 |
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
empty(..)
or! empty(...)
instead.