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) { |
||
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
|
|||
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 |
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.