loevgaard /
dandomain-consignment-bundle
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.