diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8c49de --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/vendor/ +/composer.lock +/.idea/ +/.php-version \ No newline at end of file diff --git a/DependencyInjection/SnowioCsvConnectorExtension.php b/DependencyInjection/SnowioCsvConnectorExtension.php new file mode 100644 index 0000000..61d098d --- /dev/null +++ b/DependencyInjection/SnowioCsvConnectorExtension.php @@ -0,0 +1,23 @@ +load('steps.yml'); + $loader->load('readers.yml'); + $loader->load('writers.yml'); + $loader->load('jobs.yml'); + $loader->load('form_parameters.yml'); + $loader->load('job_defaults.yml'); + $loader->load('job_constraints.yml'); + } +} diff --git a/Handler/ArchiveHandler.php b/Handler/ArchiveHandler.php new file mode 100644 index 0000000..b598322 --- /dev/null +++ b/Handler/ArchiveHandler.php @@ -0,0 +1,65 @@ +exportDir, '/') . DIRECTORY_SEPARATOR . self::ZIP_FILE_NAME; + $opened = $zip->open($location, \ZipArchive::CREATE); + if ($opened !== true) { + $this->stepExecution->addFailureException(new \RuntimeException('Failed to open zip, reason code:' . $opened)); + } else { + $success = $zip->addPattern( + '/(?:\w+\.csv|metadata.json)/', + $this->exportDir, + ['add_path' => '/', 'remove_all_path' => true] + ); + if (!$success) { + $this->stepExecution->addFailureException(new \RuntimeException('Failed to add files to zip.')); + } + $zip->close(); + $this->stepExecution->addSummaryInfo('zip_location', $location); + } + } + + public function getConfigurationFields() + { + return [ + 'exportDir' => [ + 'options' => [ + 'label' => 'snowio_connector.form.exportDir.label', + 'help' => 'snowio_connector.form.exportDir.help' + ] + ] + ]; + } + + public function setStepExecution(StepExecution $stepExecution) + { + $this->stepExecution = $stepExecution; + } + + public function getExportDir() + { + return $this->exportDir; + } + + public function setExportDir($exportDir) + { + $this->exportDir = $exportDir; + } +} diff --git a/Handler/MetadataHandler.php b/Handler/MetadataHandler.php new file mode 100644 index 0000000..f1d777f --- /dev/null +++ b/Handler/MetadataHandler.php @@ -0,0 +1,55 @@ +configs['exportDir'], '/') . DIRECTORY_SEPARATOR . 'metadata.json'; + file_put_contents( + $location, + json_encode([ + 'bundleVersion' => SnowioCsvConnectorBundle::VERSION, + 'jobCode' => $this->stepExecution->getJobExecution()->getJobInstance()->getAlias(), + 'date' => gmdate('Y-m-d_H:i:s'), + 'channel' => $this->configs['channel'], + 'delimiter' => $this->configs['delimiter'], + 'enclosure' => $this->configs['enclosure'], + 'withHeader' => $this->configs['withHeader'], + 'decimalSeparator' => $this->configs['decimalSeparator'], + 'dateFormat' => $this->configs['dateFormat'], + ]) + ); + + $this->stepExecution->addSummaryInfo('metadata_location', $location); + } + + public function getConfigurationFields() + { + return []; + } + + public function setConfiguration(array $config) + { + parent::setConfiguration($config); + $this->configs = $config; + + } + + public function setStepExecution(StepExecution $stepExecution) + { + $this->stepExecution = $stepExecution; + } + +} diff --git a/Handler/PostHandler.php b/Handler/PostHandler.php new file mode 100644 index 0000000..fa7b582 --- /dev/null +++ b/Handler/PostHandler.php @@ -0,0 +1,200 @@ + + */ + public function __construct(ClientInterface $client) + { + $this->client = $client; + } + + /** + * @throws \Exception + * @author Cristian Quiroz + */ + public function execute() + { + $url = $this->getUrl() . $this->getApplicationId(); + $this->stepExecution->addSummaryInfo('url', $url); + + $resource = fopen(rtrim($this->exportDir, '/') . DIRECTORY_SEPARATOR . ArchiveHandler::ZIP_FILE_NAME, 'r'); + + if (false === $resource) { + $this->stepExecution->addFailureException(new \RuntimeException('Failed to open file to send to snow.io')); + return; + } + + $response = $this->client->request( + 'POST', + $url, + [ + 'body' => $resource, + 'headers' => [ + 'Content-Type' => 'application/zip', + 'Authorization' => $this->getSecretKey(), + ], + ] + ); + + $this->stepExecution->addSummaryInfo('response_code', $response->getStatusCode()); + $this->stepExecution->addSummaryInfo('response_body', $response->getBody()); + + if ($response->getStatusCode() !== 204) { +// Unexpected response, handle + $this->stepExecution->addFailureException(new \Exception('Failed to POST csv data: ' . $response->getBody())); + } + + } + + /** + * @param \Akeneo\Component\Batch\Model\StepExecution $stepExecution + * @author Cristian Quiroz + */ + public function setStepExecution(StepExecution $stepExecution) + { + $this->stepExecution = $stepExecution; + } + + /** + * @param string $exportDir + * @return $this + * @author Cristian Quiroz + */ + public function setExportDir($exportDir) + { + $this->exportDir = $exportDir; + + return $this; + } + + /** + * @return string + * @author Cristian Quiroz + */ + public function getExportDir() + { + return $this->exportDir; + } + + /** + * @return string + */ + public function getApplicationId() + { + return $this->applicationId; + } + + /** + * @param string $applicationId + * @return $this + */ + public function setApplicationId($applicationId) + { + $this->applicationId = $applicationId; + + return $this; + } + + /** + * @return string + */ + public function getSecretKey() + { + return $this->secretKey; + } + + /** + * @param string $secretKey + * @return $this + */ + public function setSecretKey($secretKey) + { + $this->secretKey = $secretKey; + + return $this; + } + + /** + * @param $url + * @return $this + * @author Cristian Quiroz + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * @return string + * @author Cristian Quiroz + */ + public function getUrl() + { + return rtrim($this->url, '/') . '/'; + } + + /** + * Here, we define the form fields to use + * @return array + * @author Cristian Quiroz + */ + public function getConfigurationFields() + { + return [ + 'exportDir' => [ + 'options' => [ + 'label' => 'snowio_connector.form.exportDir.label', + 'help' => 'snowio_connector.form.exportDir.help' + ] + ], + 'url' => [ + 'type' => 'text', + 'required' => true, + 'options' => [ + 'label' => 'snowio_connector.form.url.label', + 'help' => 'snowio_connector.form.url.help' + ], + ], + 'applicationId' => [ + 'type' => 'text', + 'required' => true, + 'options' => [ + 'label' => 'snowio_connector.form.applicationId.label', + ], + ], + 'secretKey' => [ + 'type' => 'password', + 'required' => true, + 'options' => [ + 'label' => 'snowio_connector.form.secretKey.label', + ], + ], + ]; + } +} diff --git a/Job/JobParameters/ConstraintCollectionProvider/ConstraintTrait.php b/Job/JobParameters/ConstraintCollectionProvider/ConstraintTrait.php new file mode 100644 index 0000000..ff2d26e --- /dev/null +++ b/Job/JobParameters/ConstraintCollectionProvider/ConstraintTrait.php @@ -0,0 +1,49 @@ + + * @return Collection + */ + public function getConstraintCollection() + { + $constraintFields = parent::getConstraintCollection(); + $simpleFields = $constraintFields->fields; + + unset($simpleFields['filePath']); + + return new Collection(['fields' => array_merge([ + 'endpoint' => [ + new NotBlank(['groups' => ['Default', 'FileConfiguration']]), + new Regex([ + 'pattern' => '/^http(.*)\/$/', + 'message' => 'The endpoint should be an http url ending with slash. (http://localhost/)' + ]) + ], + 'applicationId' => [ + new Uuid(['groups' => ['Default', 'FileConfiguration']]), + new NotBlank(['groups' => ['Default', 'FileConfiguration']]) + ], + 'secretKey' => new NotBlank(['groups' => ['Default', 'FileConfiguration']]), + 'exportDir' => [ + new NotBlank(['groups' => ['Default', 'Execution', 'FileConfiguration']]), + new WritableDirectory(['groups' => ['Execution', 'FileConfiguration']]), + ], + 'rsyncDirectory' => [], + 'rsyncUser' => [], + 'rsyncHost' => [], + 'rsyncOptions' => [], + ], $simpleFields)]); + } +} diff --git a/Job/JobParameters/ConstraintCollectionProvider/ProductConstraint.php b/Job/JobParameters/ConstraintCollectionProvider/ProductConstraint.php new file mode 100644 index 0000000..22639f7 --- /dev/null +++ b/Job/JobParameters/ConstraintCollectionProvider/ProductConstraint.php @@ -0,0 +1,17 @@ +getName(), $this->supportedJobNames); + } +} diff --git a/Job/JobParameters/ConstraintCollectionProvider/SimpleConstraint.php b/Job/JobParameters/ConstraintCollectionProvider/SimpleConstraint.php new file mode 100644 index 0000000..6321f20 --- /dev/null +++ b/Job/JobParameters/ConstraintCollectionProvider/SimpleConstraint.php @@ -0,0 +1,17 @@ +getName(), $this->supportedJobNames); + } +} diff --git a/Job/JobParameters/DefaultValuesProvider/DefaultValuesTrait.php b/Job/JobParameters/DefaultValuesProvider/DefaultValuesTrait.php new file mode 100644 index 0000000..2e0448a --- /dev/null +++ b/Job/JobParameters/DefaultValuesProvider/DefaultValuesTrait.php @@ -0,0 +1,30 @@ + + * @return Collection + */ + public function getDefaultValues() + { + $simpleDefaults = parent::getDefaultValues(); + + unset($simpleDefaults['filePath']); + + return array_merge([ + 'endpoint' => '', + 'applicationId' => '', + 'secretKey' => '', + 'exportDir' => '', + 'rsyncDirectory'=> '', + 'rsyncUser' => '', + 'rsyncHost' => '', + 'rsyncOptions' => [], + ], $simpleDefaults); + } +} diff --git a/Job/JobParameters/DefaultValuesProvider/ProductDefaultValues.php b/Job/JobParameters/DefaultValuesProvider/ProductDefaultValues.php new file mode 100644 index 0000000..761e118 --- /dev/null +++ b/Job/JobParameters/DefaultValuesProvider/ProductDefaultValues.php @@ -0,0 +1,17 @@ +getName(), $this->supportedJobNames); + } +} diff --git a/Job/JobParameters/DefaultValuesProvider/SimpleDefaultValues.php b/Job/JobParameters/DefaultValuesProvider/SimpleDefaultValues.php new file mode 100644 index 0000000..2f134d2 --- /dev/null +++ b/Job/JobParameters/DefaultValuesProvider/SimpleDefaultValues.php @@ -0,0 +1,17 @@ +getName(), $this->supportedJobNames); + } +} diff --git a/MediaExport/ExportLocation.php b/MediaExport/ExportLocation.php new file mode 100644 index 0000000..47ac0cd --- /dev/null +++ b/MediaExport/ExportLocation.php @@ -0,0 +1,68 @@ +directory = $directory; + $this->host = $host; + $this->user = $user; + } + + /** + * @return string + */ + public function toString() + { + if ($this->user && $this->host) { + return sprintf( + '%s@%s:%s', + $this->user, + $this->host, + $this->directory + ); + } + + if (!$this->user && $this->host) { + return sprintf( + '%s:%s', + $this->host, + $this->directory + ); + } + + return $this->directory; + } + + public function setUser($user) + { + if (is_string($user) && !empty($user)) { + $this->user = $user; + } + } + + public function setDirectory($directory) + { + if (is_string($directory) && !empty($directory)) { + $this->directory = $directory; + } + } + + public function setHost($host) + { + if (is_string($host) && !empty($host)) { + $this->host = $host; + } + } +} diff --git a/MediaExport/Logger.php b/MediaExport/Logger.php new file mode 100644 index 0000000..7103011 --- /dev/null +++ b/MediaExport/Logger.php @@ -0,0 +1,51 @@ +logDirectory = $logDirectory; + } + + /** + * @param array $content + * @param $jobId + */ + public function writeLog(array $content, $jobId) + { + $logFile = $this->getLogFileNameForJob($jobId); + + if (!is_dir(dirname($logFile))) { + mkdir(dirname($logFile), 0755, true); + } + + $handle = fopen($logFile, 'a+'); + if ($handle === false) { + throw new FileNotFoundException( + sprintf('Error - log file (%s) could not be opened during media export.', $logFile) + ); + } + + fputcsv($handle, $content, PHP_EOL); + fclose($handle); + } + + /** + * @param $jobId + * @return string + */ + public function getLogFileNameForJob($jobId) + { + return rtrim($this->logDirectory, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $jobId . '.log'; + } +} diff --git a/README.md b/README.md deleted file mode 100644 index e0d7032..0000000 --- a/README.md +++ /dev/null @@ -1 +0,0 @@ -# akeneo2-snow-bundle diff --git a/Reader/Database/ProductModelReader.php b/Reader/Database/ProductModelReader.php new file mode 100644 index 0000000..19e0953 --- /dev/null +++ b/Reader/Database/ProductModelReader.php @@ -0,0 +1,13 @@ + + * + * This Bundle is used to create new csv export jobs that: + * 1) Generate CSV files, using Akeneo's Csv Connector functionality + * 2) Posts product data to a Snow.io using Guzzle + */ +class SnowioCsvConnectorBundle extends Bundle +{ + /** Increment the version number if exported data has BC break changes. */ + const VERSION = 2; +} diff --git a/Step/ArchiveStep.php b/Step/ArchiveStep.php new file mode 100644 index 0000000..98e95ab --- /dev/null +++ b/Step/ArchiveStep.php @@ -0,0 +1,81 @@ +name = $name; + $this->jobRepository = $jobRepository; + $this->eventDispatcher = $eventDispatcher; + $this->zip = $zip; + $this->mediaExportLogger = $mediaExportLogger; + } + + /** + * Archive will Zip the csv files on the specified directory + * and the metada.json file. + * + * @param StepExecution $stepExecution + */ + protected function doExecute(StepExecution $stepExecution) + { + $jobParameters = $stepExecution->getJobParameters(); + + $location = rtrim($jobParameters->get('exportDir'), '/') . DIRECTORY_SEPARATOR . self::ZIP_FILE_NAME; + + $opened = $this->zip->open($location, ZipArchive::CREATE); + + if ($opened !== true) { + $stepExecution->addFailureException(new \RuntimeException('Failed to open zip, reason code:' . $opened)); + } else { + $this->zip->addFile( + $this->mediaExportLogger->getLogFileNameForJob($stepExecution->getJobExecution()->getId()), + '/media_export.log' + ); + + $success = $this->zip->addPattern( + '/(?:\w+\.csv|metadata.json)/', + $jobParameters->get('exportDir'), + ['add_path' => '/', 'remove_all_path' => true] + ); + + if (!$success) { + $stepExecution->addFailureException(new \RuntimeException('Failed to add files to zip.')); + return; + } + + $this->zip->close(); + + $stepExecution->addSummaryInfo('zip_location', $location); + } + } +} diff --git a/Step/CheckThresholdsStep.php b/Step/CheckThresholdsStep.php new file mode 100644 index 0000000..c896e09 --- /dev/null +++ b/Step/CheckThresholdsStep.php @@ -0,0 +1,95 @@ +minimumExportThreshold = (int)$minimumExportThreshold; + } + + /** + * Extension point for subclasses to execute business logic. Subclasses should set the {@link ExitStatus} on the + * {@link StepExecution} before returning. + * + * Do not catch exception here. It will be correctly handled by the execute() method. + * + * @param StepExecution $stepExecution the current step context + * + * @throws \Exception + */ + protected function doExecute(StepExecution $stepExecution) + { + $previousStepExecution = $this->getPreviousStepExecution($stepExecution); + + $stepExecution->addSummaryInfo( + 'minimum_threshold', + sprintf('%s (%s)', $this->minimumExportThreshold, $previousStepExecution->getStepName()) + ); + + if ($this->minimumExportThreshold > 0 && !$this->doesExportCountMeetThreshold($previousStepExecution)) { + throw new \Exception( + sprintf( + 'Error - attempted to export less than the minimum threshold (step: %s/threshold: %s).', + $previousStepExecution->getStepName(), + $this->minimumExportThreshold + ) + ); + } + } + + /** + * @param StepExecution $stepExecution + * @return bool + * @author James Pollard + */ + private function doesExportCountMeetThreshold(StepExecution $stepExecution) + { + return $stepExecution->getSummaryInfo('read') >= $this->minimumExportThreshold; + } + + /** + * @param StepExecution $stepExecution + * @return StepExecution + * @throws \Exception + * @author James Pollard + */ + private function getPreviousStepExecution(StepExecution $stepExecution) + { + $stepExecutions = $stepExecution->getJobExecution()->getStepExecutions()->toArray(); + // set array pointer to last element i.e. the current execution + end($stepExecutions); + $previousStepExecution = prev($stepExecutions); + + if (!($previousStepExecution instanceof StepExecution)) { + throw new \Exception('Error during threshold check step - previous execution step was not found.'); + } + + return $previousStepExecution; + } +} diff --git a/Step/MediaExportStep.php b/Step/MediaExportStep.php new file mode 100644 index 0000000..0b9124a --- /dev/null +++ b/Step/MediaExportStep.php @@ -0,0 +1,152 @@ +exportLocation = $exportLocation; + $this->logger = $logger; + } + + /** + * Extension point for subclasses to execute business logic. Subclasses should set the {@link ExitStatus} on the + * {@link StepExecution} before returning. + * + * Do not catch exception here. It will be correctly handled by the execute() method. + * + * @param StepExecution $stepExecution the current step context + * + * @throws \Exception + */ + protected function doExecute(StepExecution $stepExecution) + { + try { + $currentExportDir = rtrim($stepExecution->getJobParameters()->get('exportDir'), '/'); + + $this->exportLocation->setUser($stepExecution->getJobParameters()->get('rsyncUser')); + $this->exportLocation->setHost($stepExecution->getJobParameters()->get('rsyncHost')); + $this->exportLocation->setDirectory($stepExecution->getJobParameters()->get('rsyncDirectory')); + + $newExportDir = rtrim($this->exportLocation->toString(), '/'); + + $stepExecution->addSummaryInfo('export_location', $newExportDir); + + $stepExecution->addSummaryInfo( + 'log_file', + $this->logger->getLogFileNameForJob($stepExecution->getJobExecution()->getId()) + ); + + $output = $this->syncMedia($currentExportDir, $newExportDir, $stepExecution->getJobParameters()->get('rsyncOptions')); + + $this->writeLog( + $this->getModifiedOutputForLog($output, $stepExecution), + $stepExecution->getJobExecution() + ); + + } catch (FileTransferException $e) { + $this->writeLog( + ['Error - something went wrong during rsync.', $e->getMessage()], + $stepExecution->getJobExecution() + ); + + //Do not rethrow the exception we want execution to proceed + } catch(\Exception $e) { + $this->writeLog( + ['Error - something went wrong during media export.', $e->getMessage()], + $stepExecution->getJobExecution() + ); + throw $e; + } + } + + /** + * @param $currentExportDir + * @param $newExportDir + * @param $options + * @return array + * @throws FileTransferException + * @author James Pollard + */ + protected function syncMedia($currentExportDir, $newExportDir, $options = '') + { + /** + * append files to the current export dir so that we do not unnecessarily + * copy over the export csv files + */ + exec("rsync -aK $options $currentExportDir/files/ $newExportDir/", $output, $status); + + if ($status !== 0) { + throw new FileTransferException('Error - rsync failure during media export.' . implode(" : ", $output)); + } + + return $output; + } + + /** + * @param array $content + * @author James Pollard + */ + protected function writeLog(array $content, JobExecution $job) + { + $this->logger->writeLog($content, $job->getId()); + } + + /** + * @param array $output + * @param StepExecution $stepExecution + * @return array + * @author James Pollard + */ + protected function getModifiedOutputForLog(array $output, StepExecution $stepExecution) + { + $jobParameters = $stepExecution->getJobParameters(); + $jobExecution = $stepExecution->getJobExecution(); + + array_unshift( + $output, + '------------------------------', + sprintf('Export Profile: %s (%s)', $jobExecution->getLabel(), $jobParameters->get('applicationId')), + sprintf('Execution ID: %s', $jobExecution->getId()), + date('d/m/Y H:i:s') + ); + + return $output; + } +} diff --git a/Step/MetadataStep.php b/Step/MetadataStep.php new file mode 100644 index 0000000..69ea8ca --- /dev/null +++ b/Step/MetadataStep.php @@ -0,0 +1,70 @@ +getJobParameters(); + + $location = rtrim($jobParameters->get('exportDir'), '/') . DIRECTORY_SEPARATOR . 'metadata.json'; + + $content = $this->generateContent($stepExecution, $jobParameters); + + if (false === file_put_contents($location, $content)) { + $stepExecution->addFailureException( + new \RuntimeException('Cannot create metadata file.') + ); + } + + $stepExecution->addSummaryInfo('metadata_location', $location); + } + + /** + * Create json file with metadata + * + * @param StepExecution $stepExecution + * @param JobParameters $jobParameters + * + * @return String Json + */ + protected function generateContent($stepExecution, $jobParameters) + { + $content = [ + 'bundleVersion' => SnowioCsvConnectorBundle::VERSION, + 'jobCode' => $stepExecution->getJobExecution()->getJobInstance()->getJobName(), + 'date' => gmdate('Y-m-d H:i:s'), + 'delimiter' => $jobParameters->get('delimiter'), + 'enclosure' => $jobParameters->get('enclosure'), + 'withHeader' => $jobParameters->get('withHeader') + ]; + + if ($jobParameters->has('filters')) { + $content['filters'] = $jobParameters->get('filters'); + } + + if ($jobParameters->has('decimalSeparator')) { + $content['decimalSeparator'] = $jobParameters->get('decimalSeparator'); + } + + if ($jobParameters->has('dateFormat')) { + $content['dateFormat'] = $jobParameters->get('dateFormat'); + } + + if ($jobParameters->has('with_media')) { + $content['with_media'] = $jobParameters->get('with_media'); + } + + return json_encode($content); + } +} diff --git a/Step/PostStep.php b/Step/PostStep.php new file mode 100644 index 0000000..95e3074 --- /dev/null +++ b/Step/PostStep.php @@ -0,0 +1,81 @@ +name = $name; + $this->jobRepository = $jobRepository; + $this->eventDispatcher = $eventDispatcher; + $this->guzzle = $guzzle; + } + + /** + * Post Step will send the zip file generated on preview step + * to Snow.io + * + * @author Nei Rauni + * @param StepExecution $stepExecution + */ + protected function doExecute(StepExecution $stepExecution) + { + $jobParameters = $stepExecution->getJobParameters(); + $endpoint = $jobParameters->get('endpoint') . $jobParameters->get('applicationId'); + + $stepExecution->addSummaryInfo('endpoint', $endpoint); + + $zipFile = rtrim($jobParameters->get('exportDir'), '/') . DIRECTORY_SEPARATOR . ArchiveStep::ZIP_FILE_NAME; + + if (!file_exists($zipFile)) { + $stepExecution->addFailureException( + new \RuntimeException('Failed to open file '.$zipFile.' to send to snow.io') + ); + } + + if (($resource = fopen($zipFile, 'r')) === false) { + $stepExecution->addFailureException( + new \RuntimeException('Failed to open file '.$zipFile.' to send to snow.io') + ); + } + + $response = $this->guzzle->request( + 'POST', + $endpoint, + [ + 'body' => $resource, + 'headers' => [ + 'Content-Type' => 'application/zip', + 'Authorization' => $jobParameters->get('secretKey'), + ], + ] + ); + + if ($response->getStatusCode() !== 204) { + $stepExecution->addFailureException(new \Exception('Failed to POST CSV file: ' . $response->getBody())); + } + + $stepExecution->addSummaryInfo('response_code', $response->getStatusCode()); + $stepExecution->addSummaryInfo('response_body', $response->getBody()->getContents()); + } +} diff --git a/Writer/File/Csv/ProductModelWriter.php b/Writer/File/Csv/ProductModelWriter.php new file mode 100644 index 0000000..4559c11 --- /dev/null +++ b/Writer/File/Csv/ProductModelWriter.php @@ -0,0 +1,10 @@ + + * @return string + */ + public function getPath(array $placeholders = []) + { + $parameters = $this->stepExecution->getJobParameters(); + + $filePath = implode( + '', + [ + rtrim($parameters->get('exportDir'), '/'), + DIRECTORY_SEPARATOR, + $this->sanitize($this->stepExecution->getStepName()), + ".csv" + ] + ); + + return $filePath; + } + + /** + * Export medias from the working directory to the output expected directory. + * + * Basically, we first remove the content of /path/where/my/user/expects/the/export/files/. + * (This path can exist of an export was launched previously) + * + * Then we copy /path/of/the/working/directory/files/ to /path/where/my/user/expects/the/export/files/. + */ + protected function exportMedias() + { + $outputDirectory = dirname($this->getPath()); + $workingDirectory = $this->stepExecution->getJobExecution()->getExecutionContext() + ->get(JobInterface::WORKING_DIRECTORY_PARAMETER); + + $outputFilesDirectory = $outputDirectory . DIRECTORY_SEPARATOR . 'files'; + $workingFilesDirectory = $workingDirectory . 'files'; + + /* + if ($this->localFs->exists($outputFilesDirectory)) { + $this->localFs->remove($outputFilesDirectory); + }*/ + + if ($this->localFs->exists($workingFilesDirectory)) { + $this->localFs->mirror($workingFilesDirectory, $outputFilesDirectory); + } + } +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2eae2af --- /dev/null +++ b/composer.json @@ -0,0 +1,41 @@ +{ + "name": "snowio/akeneo2-snow-bundle", + "license": "MIT", + "authors": [ + { + "name": "Cristian Quiroz", + "email": "cq@amp.co" + }, { + "name": "Nei Santos", + "email": "ns@amp.co" + }, { + "name": "Liam Toohey", + "email": "lt@amp.co" + } + ], + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/akeneo/pim-community-dev.git", + "branch": "master" + } + ], + "require": { + "akeneo/pim-community-dev": "2.*.*", + "guzzlehttp/guzzle": "^6.1", + "symfony/config": "^3.4.0", + "symfony/dependency-injection": "^3.4.0", + "symfony/http-kernel": "^3.4.0", + "phpspec/phpspec": "^3.2" + }, + "scripts": { + "test": "php vendor/bin/phpspec run --format=dot -c phpspec.yml", + "cs": "phpcs --standard=PSR2 -n ./ --ignore=vendor --report=summary" + }, + "autoload": { + "psr-4": { + "Snowio\\Bundle\\CsvConnectorBundle\\": "" + } + }, + "minimum-stability": "dev" +} \ No newline at end of file diff --git a/spec/Job/JobParameters/ConstraintCollectionProvider/ProductConstraintSpec.php b/spec/Job/JobParameters/ConstraintCollectionProvider/ProductConstraintSpec.php new file mode 100644 index 0000000..1ae3ddf --- /dev/null +++ b/spec/Job/JobParameters/ConstraintCollectionProvider/ProductConstraintSpec.php @@ -0,0 +1,54 @@ +beConstructedWith( + $provider, + ['test11','test22'] + ); + } + + // @codingStandardsIgnoreLine + public function it_is_a_default_values() + { + $this->shouldImplement('Akeneo\Component\Batch\Job\JobParameters\ConstraintCollectionProviderInterface'); + } + + // @codingStandardsIgnoreLine + public function it_supports_a_job(JobInterface $job) + { + $job->getName()->willReturn('test11'); + $this->supports($job)->shouldReturn(true); + } + + // @codingStandardsIgnoreLine + public function it_provides_constraints_collection( + $provider, + Collection $decoratedCollection + ) { + $provider->getConstraintCollection()->willReturn($decoratedCollection); + $collection = $this->getConstraintCollection(); + $collection->shouldReturnAnInstanceOf('Symfony\Component\Validator\Constraints\Collection'); + $fields = $collection->fields; + + $fields->shouldHaveCount(8); + $fields->shouldHaveKey('decimalSeparator'); + $fields->shouldHaveKey('dateFormat'); + $fields->shouldHaveKey('with_media'); + $fields->shouldHaveKey('endpoint'); + $fields->shouldHaveKey('applicationId'); + $fields->shouldHaveKey('secretKey'); + $fields->shouldHaveKey('exportDir'); + } +} diff --git a/spec/Job/JobParameters/DefaultValuesProvider/ProductDefaultValuesSpec.php b/spec/Job/JobParameters/DefaultValuesProvider/ProductDefaultValuesSpec.php new file mode 100644 index 0000000..99808dd --- /dev/null +++ b/spec/Job/JobParameters/DefaultValuesProvider/ProductDefaultValuesSpec.php @@ -0,0 +1,67 @@ +beConstructedWith($decoratedProvider, $channelRepository, $localeRepository, ['my_supported_job_name']); + } + + // @codingStandardsIgnoreLine + function it_is_a_default_values() + { + $this->shouldImplement('Akeneo\Component\Batch\Job\JobParameters\DefaultValuesProviderInterface'); + } + + // @codingStandardsIgnoreLine + function it_supports_a_job(JobInterface $job) + { + $job->getName()->willReturn('my_supported_job_name'); + $this->supports($job)->shouldReturn(true); + } + + // @codingStandardsIgnoreLine + function it_provides_default_values( + $decoratedProvider, + ChannelRepositoryInterface $channelRepository, + LocaleRepositoryInterface $localeRepository, + LocaleInterface $locale, + ChannelInterface $channel + ) { + $channel->getCode()->willReturn('channel_code'); + $channelRepository->getFullChannels()->willReturn([$channel]); + + $locale->getCode()->willReturn('locale_code'); + $localeRepository->getActivatedLocaleCodes()->willReturn([$locale]); + + $decoratedProvider->getDefaultValues()->willReturn(['decoratedParam' => true]); + $this->getDefaultValues()->shouldReturnWellFormedDefaultValues(); + } + + public function getMatchers() + { + return [ + 'returnWellFormedDefaultValues' => function ($parameters) { + return true === $parameters['decoratedParam'] && + '.' === $parameters['decimalSeparator'] && + '' === $parameters['endpoint'] && + '' === $parameters['secretKey'] && + '' === $parameters['applicationId'] && + '' === $parameters['exportDir']; + } + ]; + } +} diff --git a/spec/Step/ArchiveStepSpec.php b/spec/Step/ArchiveStepSpec.php new file mode 100644 index 0000000..950ecc6 --- /dev/null +++ b/spec/Step/ArchiveStepSpec.php @@ -0,0 +1,119 @@ +directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'spec' . DIRECTORY_SEPARATOR; + + $this->beConstructedWith(['job_test_name'], $dispatcher, $respository, $zip); + } + + // @codingStandardsIgnoreLine + public function it_executes_with_success( + StepExecution $execution, + $dispatcher, + $respository, + JobParameters $jobParameters, + BatchStatus $status, + ExitStatus $exitStatus, + $zip + ) { + $jobParameters->get('exportDir')->willReturn($this->directory); + $execution->getJobParameters()->willReturn($jobParameters); + $execution->getJobExecution()->willReturn($execution); + + # before + $execution->getStatus()->willReturn($status); + $status->getValue()->willReturn(BatchStatus::STARTING); + $dispatcher->dispatch(EventInterface::BEFORE_STEP_EXECUTION, Argument::any())->shouldBeCalled(); + $execution->setStartTime(Argument::any())->shouldBeCalled(); + $execution->setStatus(Argument::any())->shouldBeCalled(); + $execution->upgradeStatus(Argument::any())->shouldBeCalled(); + + $zip->open(Argument::any(), Argument::any())->willReturn(true); + $zip->addPattern(Argument::any(), Argument::any(), Argument::any())->willReturn(array( + '/tmp/metadata.json', + '/tmp/anotherfile.csv' + )); + $zip->close()->shouldBeCalled(); + + $execution->addSummaryInfo('zip_location', $this->directory.'export.zip')->shouldBeCalled(); + + # after + $execution->getExitStatus()->willReturn($exitStatus); + $exitStatus->getExitCode()->willReturn(ExitStatus::COMPLETED); + $execution->isTerminateOnly()->willReturn(false); + $execution->upgradeStatus(Argument::any())->shouldBeCalled(); + $dispatcher->dispatch(EventInterface::STEP_EXECUTION_SUCCEEDED, Argument::any())->shouldBeCalled(); + $dispatcher->dispatch(EventInterface::STEP_EXECUTION_COMPLETED, Argument::any())->shouldBeCalled(); + $execution->setEndTime(Argument::any())->shouldBeCalled(); + $execution->setExitStatus(Argument::any())->shouldBeCalled(); + + $this->execute($execution); + } + + // @codingStandardsIgnoreLine + public function it_throws_add_failure_exception_when_there_is_none_mathed_files( + StepExecution $execution, + $dispatcher, + $respository, + JobParameters $jobParameters, + BatchStatus $status, + ExitStatus $exitStatus, + $zip + ) { + + $jobParameters->get('exportDir')->willReturn($this->directory); + $execution->getJobParameters()->willReturn($jobParameters); + $execution->getJobExecution()->willReturn($execution); + + # before + $execution->getStatus()->willReturn($status); + $status->getValue()->willReturn(BatchStatus::STARTING); + $dispatcher->dispatch(EventInterface::BEFORE_STEP_EXECUTION, Argument::any())->shouldBeCalled(); + $execution->setStartTime(Argument::any())->shouldBeCalled(); + $execution->setStatus(Argument::any())->shouldBeCalled(); + $execution->upgradeStatus(Argument::any())->shouldBeCalled(); + + $zip->open(Argument::any(), Argument::any())->willReturn(true); + + // return empty array + $zip->addPattern(Argument::any(), Argument::any(), Argument::any())->willReturn(array()); + + $execution->addFailureException(Argument::any())->shouldBeCalled(); + + # after + $execution->getExitStatus()->willReturn($exitStatus); + $exitStatus->getExitCode()->willReturn(ExitStatus::COMPLETED); + $execution->isTerminateOnly()->willReturn(false); + $execution->upgradeStatus(Argument::any())->shouldBeCalled(); + $dispatcher->dispatch(EventInterface::STEP_EXECUTION_SUCCEEDED, Argument::any())->shouldBeCalled(); + $dispatcher->dispatch(EventInterface::STEP_EXECUTION_COMPLETED, Argument::any())->shouldBeCalled(); + $execution->setEndTime(Argument::any())->shouldBeCalled(); + $execution->setExitStatus(Argument::any())->shouldBeCalled(); + + $this->execute($execution); + } +} diff --git a/spec/Step/MetadataStepSpec.php b/spec/Step/MetadataStepSpec.php new file mode 100644 index 0000000..c2546be --- /dev/null +++ b/spec/Step/MetadataStepSpec.php @@ -0,0 +1,89 @@ +directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'spec' . DIRECTORY_SEPARATOR; + $this->filesystem = new Filesystem(); + $this->filesystem->mkdir($this->directory); + + $this->beConstructedWith(['job_test_name'], $dispatcher, $respository); + } + + // @codingStandardsIgnoreLine + function it_executes_with_success( + StepExecution $execution, + $dispatcher, + JobParameters $jobParameters, + BatchStatus $status, + ExitStatus $exitStatus, + JobInstance $jobInstance, + JobExecution $jobExecution + ) { + $jobParameters->get('exportDir')->willReturn($this->directory); + $jobParameters->get('delimiter')->willReturn('abc'); + $jobParameters->get('filters')->willReturn('abc'); + $jobParameters->has('filters')->willReturn(true); + $jobParameters->get('enclosure')->willReturn('abc'); + $jobParameters->get('withHeader')->willReturn('abc'); + $jobParameters->get('decimalSeparator')->willReturn('abc'); + $jobParameters->has('decimalSeparator')->willReturn(true); + $jobParameters->get('dateFormat')->willReturn('abc'); + $jobParameters->has('dateFormat')->willReturn(true); + $jobParameters->get('with_media')->willReturn('abc'); + $jobParameters->has('with_media')->willReturn(true); + + $execution->getJobParameters()->willReturn($jobParameters); + + $jobInstance->getJobName()->willReturn('Jobtest'); + $jobExecution->getJobInstance()->willReturn($jobInstance); + $execution->getJobExecution()->willReturn($jobExecution); + + # before + $execution->getStatus()->willReturn($status); + $status->getValue()->willReturn(BatchStatus::STARTING); + $dispatcher->dispatch(EventInterface::BEFORE_STEP_EXECUTION, Argument::any())->shouldBeCalled(); + $execution->setStartTime(Argument::any())->shouldBeCalled(); + $execution->setStatus(Argument::any())->shouldBeCalled(); + $execution->upgradeStatus(Argument::any())->shouldBeCalled(); + + # my step logic assertions + $execution->addSummaryInfo('metadata_location', $this->directory.'metadata.json')->shouldBeCalled(); + + # after + $execution->getExitStatus()->willReturn($exitStatus); + $exitStatus->getExitCode()->willReturn(ExitStatus::COMPLETED); + $execution->isTerminateOnly()->willReturn(false); + $execution->upgradeStatus(Argument::any())->shouldBeCalled(); + $dispatcher->dispatch(EventInterface::STEP_EXECUTION_SUCCEEDED, Argument::any())->shouldBeCalled(); + $dispatcher->dispatch(EventInterface::STEP_EXECUTION_COMPLETED, Argument::any())->shouldBeCalled(); + $execution->setEndTime(Argument::any())->shouldBeCalled(); + $execution->setExitStatus(Argument::any())->shouldBeCalled(); + + $this->execute($execution); + } +} diff --git a/spec/Step/PostStepSpec.php b/spec/Step/PostStepSpec.php new file mode 100644 index 0000000..9b506de --- /dev/null +++ b/spec/Step/PostStepSpec.php @@ -0,0 +1,100 @@ +directory = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'spec' . DIRECTORY_SEPARATOR; + $this->filesystem = new Filesystem(); + + $this->beConstructedWith(['job_test_name'], $dispatcher, $respository, $guzzle); + } + + public function letGo() + { + $this->filesystem->remove($this->directory); + } + + // @codingStandardsIgnoreLine + function it_executes_with_success( + StepExecution $execution, + $dispatcher, + $respository, + JobParameters $jobParameters, + BatchStatus $status, + ExitStatus $exitStatus, + Client $guzzle, + Response $response, + StreamInterface $body + ) { + $jobParameters->get('endpoint')->willReturn('myendpointfortest'); + $jobParameters->get('exportDir')->willReturn($this->directory); + $jobParameters->get('secretKey')->willReturn('abc'); + $jobParameters->get('applicationId')->willReturn('999'); + + $file = $this->directory.'export.zip'; + $resource = fopen($file, 'w+'); + + $execution->getJobParameters()->willReturn($jobParameters); + $execution->getJobExecution()->willReturn($execution); + + # before + $execution->getStatus()->willReturn($status); + $status->getValue()->willReturn(BatchStatus::STARTING); + $dispatcher->dispatch(EventInterface::BEFORE_STEP_EXECUTION, Argument::any())->shouldBeCalled(); + $execution->setStartTime(Argument::any())->shouldBeCalled(); + $execution->setStatus(Argument::any())->shouldBeCalled(); + $execution->upgradeStatus(Argument::any())->shouldBeCalled(); + + # my step logic assertions + $execution->addSummaryInfo('endpoint', 'myendpointfortest999')->shouldBeCalled(); + $execution->addSummaryInfo('response_code', '204')->shouldBeCalled(); + $execution->addSummaryInfo('response_body', 'data received')->shouldBeCalled(); + + $response->getStatusCode()->willReturn(204); + + $body->getContents()->willReturn('data received'); + $response->getBody()->willReturn($body); + $guzzle->request('POST', 'myendpointfortest999', Argument::any())->willReturn($response); + + # after + $execution->getExitStatus()->willReturn($exitStatus); + $exitStatus->getExitCode()->willReturn(ExitStatus::COMPLETED); + $execution->isTerminateOnly()->willReturn(false); + $execution->upgradeStatus(Argument::any())->shouldBeCalled(); + $dispatcher->dispatch(EventInterface::STEP_EXECUTION_SUCCEEDED, Argument::any())->shouldBeCalled(); + $dispatcher->dispatch(EventInterface::STEP_EXECUTION_COMPLETED, Argument::any())->shouldBeCalled(); + $execution->setEndTime(Argument::any())->shouldBeCalled(); + $execution->setExitStatus(Argument::any())->shouldBeCalled(); + + $this->execute($execution); + + //remove file after test + unlink($file); + } +}