diff --git a/app/composer.json b/app/composer.json index 5ab5900..6783f0c 100644 --- a/app/composer.json +++ b/app/composer.json @@ -28,12 +28,18 @@ } }, "require-dev": { - "squizlabs/php_codesniffer": "^3.5", + "squizlabs/php_codesniffer": "^3.7", "phpunit/phpunit": "^9.5", - "opsway/psr12-strict-coding-standard": "^0.5.0" + "opsway/psr12-strict-coding-standard": "^0.5.0", + "phpcompatibility/php-compatibility": "^9.3" }, "scripts": { - "cs": "phpcs", + "cs": "phpcs --colors --standard=PSR12", "cbf": "phpcbf" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/app/composer.lock b/app/composer.lock index 262955b..ee8d9fe 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "81050635de2f87c3f7f693ec8cb30645", + "content-hash": "7b0be09c8f282dfcceb075d1455caa28", "packages": [ { "name": "doctrine/lexer", @@ -2949,6 +2949,68 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpcompatibility/php-compatibility", + "version": "9.3.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/9fb324479acf6f39452e0655d2429cc0d3914243", + "reference": "9fb324479acf6f39452e0655d2429cc0d3914243", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "squizlabs/php_codesniffer": "^2.3 || ^3.0.2" + }, + "conflict": { + "squizlabs/php_codesniffer": "2.6.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.5 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically.", + "roave/security-advisories": "dev-master || Helps prevent installing dependencies with known security issues." + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "http://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "time": "2019-12-27T09:44:58+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -4659,16 +4721,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.8", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4" + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", - "reference": "9d583721a7157ee997f235f327de038e7ea6dac4", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", "shasum": "" }, "require": { @@ -4706,7 +4768,12 @@ "phpcs", "standards" ], - "time": "2020-10-23T02:01:07+00:00" + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2022-06-18T07:21:10+00:00" }, { "name": "theseer/tokenizer", @@ -4869,5 +4936,5 @@ "prefer-lowest": false, "platform": [], "platform-dev": [], - "plugin-api-version": "2.1.0" + "plugin-api-version": "2.3.0" } diff --git a/app/src/Commands/InvoiceCommand.php b/app/src/Commands/InvoiceCommand.php index dcc229e..760f78c 100644 --- a/app/src/Commands/InvoiceCommand.php +++ b/app/src/Commands/InvoiceCommand.php @@ -6,6 +6,7 @@ declare(strict_types=1); namespace RprtCli\Commands; +use Exception; use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\CsvReport\ReportCsvInterface; use RprtCli\Utils\Mailer\MailerInterface; @@ -15,14 +16,24 @@ use RprtCli\ValueObjects\Expenses; use RprtCli\ValueObjects\WorkInvoiceElement; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -// use Symfony\Contracts\Translation\TranslatorInterface; +use function array_merge; +use function explode; +use function is_array; +use function is_null; +use function is_string; +use function readline; +use function strpos; +use function strtolower; use function var_dump; +use function var_export; + +// use Symfony\Contracts\Translation\TranslatorInterface; /** * Main file - invoice command. @@ -37,7 +48,7 @@ class InvoiceCommand extends Command protected $pdfExport; - const TYPE_WORK = 1; + const TYPE_WORK = 1; const TYPE_EXPENSE = 2; public function __construct( @@ -51,8 +62,8 @@ class InvoiceCommand extends Command $this->csv = $csv; $this->configuration = $configuration; $this->youtrack = $youtrack; - $this->pdfExport = $pdf_export; - $this->mailer = $mailer; + $this->pdfExport = $pdf_export; + $this->mailer = $mailer; parent::__construct($name); } @@ -112,14 +123,14 @@ class InvoiceCommand extends Command 'e', InputOption::VALUE_OPTIONAL, 'List of additional expenses in format expense1=value1;expenses2=value2... or empty for interactive output.', - FALSE + false ); $this->addOption( 'custom', 'c', InputOption::VALUE_OPTIONAL, 'Additional custom work untracked in format: name1=time1;name2=time2... Project to assign work items to has to be configured in app config. Leave empty for interactive output.', - FALSE + false ); $this->addOption( 'list-reports', @@ -132,7 +143,7 @@ class InvoiceCommand extends Command 'r', InputOption::VALUE_OPTIONAL, 'Show time tracked for report.', - FALSE + false ); } @@ -144,16 +155,15 @@ class InvoiceCommand extends Command } if ($input->getOption('list-reports')) { $list = $this->youtrack->listReports(); - $output->writeln(var_export($list, TRUE)); + $output->writeln(var_export($list, true)); return Command::SUCCESS; } if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) { if ($report = $input->getOption('report')) { $this->youtrack->setReportId($report); - } - else { + } else { $reports = $this->youtrack->listReports(); - $count = 1; + $count = 1; foreach ($reports as $id => $name) { $output->writeln("[{$count}] {$name} ({$id})"); $count++; @@ -167,7 +177,7 @@ class InvoiceCommand extends Command } } if ($youtrack = $input->getOption('youtrack')) { - $report_id = $this->youtrack->getReportId(); + $report_id = $this->youtrack->getReportId(); $cache_clear_status = $this->youtrack->clearReportCache($report_id); if ($output->isVerbose()) { $output->writeln("Report {$report_id} cache cleared, status: {$cache_clear_status}"); @@ -185,8 +195,8 @@ class InvoiceCommand extends Command if ($output->isVerbose()) { $output->writeln("Csv file downloaded to: {$file}"); } - $data = $this->csv->getInvoiceData($file); - if (!empty($expenses)) { + $data = $this->csv->getInvoiceData($file); + if (! empty($expenses)) { $data = array_merge($data, $expenses); } // $table = $this->generateTable($output, $data); @@ -219,17 +229,20 @@ class InvoiceCommand extends Command return Command::SUCCESS; } - protected function getTable(OutputInterface $output, array $data) :Table { - $rows = $this->csv->generateTable($data); + protected function getTable(OutputInterface $output, array $data) : Table + { + $rows = $this->csv->generateTable($data); $table = new Table($output); $table->setHeaders([ - 'Project', 'Hours', 'Rate', 'Price', + 'Project', + 'Hours', + 'Rate', + 'Price', ]); foreach ($rows as $key => $row) { - if (!$row) { + if (! $row) { $rows[$key] = new TableSeparator(); - } - elseif (is_array($row) && is_null($row[1]) && is_null($row[0])) { + } elseif (is_array($row) && is_null($row[1]) && is_null($row[0])) { // Check which elements in array are null. $rows[$key] = [new TableCell($row[2], ['colspan' => 3]), $row[3]]; } @@ -248,7 +261,10 @@ class InvoiceCommand extends Command { $table = new Table($output); $table->setHeaders([ - 'Project', 'Hours', 'Rate', 'Price', + 'Project', + 'Hours', + 'Rate', + 'Price', ]); [$rows, $totalHours, $totalPrice] = [[], 0, 0]; $projectsConfig = $this->configuration->get('projects'); @@ -274,9 +290,9 @@ class InvoiceCommand extends Command $totalPrice += $price; unset($data[$name]); } - if (!empty($data)) { + if (! empty($data)) { foreach ($data as $name => $value) { - if (strpos(strtolower($name), 'expanses') !== FALSE) { + if (strpos(strtolower($name), 'expanses') !== false) { } } } @@ -314,65 +330,66 @@ class InvoiceCommand extends Command * * @return Expenses[] */ - protected function getExpenses($expenses) { + protected function getExpenses($expenses) + { $output = []; if (is_string($expenses)) { foreach (explode(';', $expenses) as $expense) { [$name, $value] = explode('=', $expense); - $output[] = new Expenses($name, (float) $value); + $output[] = new Expenses($name, (float) $value); } - } - else { - $continue = TRUE; + } else { + $continue = true; while ($continue) { - $name = readline('Enter expenses name or leave empty to stop: '); + $name = readline('Enter expenses name or leave empty to stop: '); $value = (float) readline('Enter expenses value: '); - if (!empty($name)) { + if (! empty($name)) { $output[] = new Expenses($name, $value); - } - else { - $continue = FALSE; + } else { + $continue = false; } } } return $output; } - protected function getCustomWorkOrExpenses($custom, $type) { + protected function getCustomWorkOrExpenses($custom, $type) + { $output = []; if (is_string($custom)) { foreach (explode(';', $custom) as $item) { [$name, $value] = explode('=', $item); - $output[] = $this->createInvoiceElement($name, (float) $value, $type); + $output[] = $this->createInvoiceElement($name, (float) $value, $type); } } else { - $continue = TRUE; - if ($type == self::TYPE_WORK) { - $message_name = 'Enter project name or leave empty to stop: '; + $continue = true; + if ($type === self::TYPE_WORK) { + $message_name = 'Enter project name or leave empty to stop: '; $message_value = 'Enter time spent of project: '; - } elseif ($type == self::TYPE_EXPENSE) { - $message_name = 'Enter expenses name or leave empty to stop: '; + } elseif ($type === self::TYPE_EXPENSE) { + $message_name = 'Enter expenses name or leave empty to stop: '; $message_value = 'Enter expenses value: '; } while ($continue) { - $name = readline($message_name); + $name = readline($message_name); $value = (float) readline($message_value); - if (!empty($name)) { + if (! empty($name)) { $output[] = $this->createInvoiceElement($name, $value, $type); } else { - $continue = FALSE; + $continue = false; } } } return $output; } - protected function createInvoiceElement(string $name, float $value, int $type) { - if ($type == self::TYPE_WORK) { + protected function createInvoiceElement(string $name, float $value, int $type) + { + if ($type === self::TYPE_WORK) { return new WorkInvoiceElement($name, (float) $value); - } elseif ($type == self::TYPE_EXPENSE) { + } elseif ($type === self::TYPE_EXPENSE) { return new Expenses($name, (float) $value); } - throw new \Exception('Unkown invoice element type.'); + throw new Exception('Unkown invoice element type.'); } } diff --git a/app/src/Commands/ReportCommand.php b/app/src/Commands/ReportCommand.php index 7dc9fee..111b4df 100644 --- a/app/src/Commands/ReportCommand.php +++ b/app/src/Commands/ReportCommand.php @@ -9,29 +9,36 @@ use RprtCli\Utils\CsvReport\ReportCsvInterface; use RprtCli\Utils\TimeTrackingServices\YoutrackInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; -use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Helper\TableCell; +use Symfony\Component\Console\Helper\TableSeparator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ChoiceQuestion; -class ReportCommand extends Command { +use function array_flip; +use function is_array; +use function is_null; +class ReportCommand extends Command +{ protected $trackingService; protected $config; protected $csv; - public function __construct(ConfigurationInterface $configuration, YoutrackInterface $tracking_service, ReportCsvInterface $csv, ?string $name = null) { + public function __construct(ConfigurationInterface $configuration, YoutrackInterface $tracking_service, ReportCsvInterface $csv, ?string $name = null) + { $this->config = $configuration; // @TODO generalize tracking service. $this->trackingService = $tracking_service; - $this->csv = $csv; + $this->csv = $csv; parent::__construct($name); } - protected function configure() :void { + protected function configure() : void + { $this->setName('report'); $this->setDescription('Get a time-tracking report into command line.'); $this->addOption( @@ -48,7 +55,8 @@ class ReportCommand extends Command { ); } - protected function execute(InputInterface $input, OutputInterface $output) :int { + protected function execute(InputInterface $input, OutputInterface $output) : int + { if ($timeRange = $input->getOption('time-range')) { // @TODO: Implement time range option: // - Request workTime items from tracking service @@ -62,29 +70,31 @@ class ReportCommand extends Command { $reports = $this->trackingService->listReports(); // Could just parse a csv file or actually get workItems from youtrack ... if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) { - if ($report = $input->getOption('report')) { - $this->trackingService->setReportId($report); - } else { - $count = 1; - foreach ($reports as $id => $name) { - $output->writeln("[{$count}] {$name} ({$id})"); - $count++; - } - $output->writeln("[{$count}] None (null)"); - $report = readline('Select id of the report: '); - // Asume people are literate. - if (!in_array($report, array_keys($reports) )) { - $output->writeln('Non-existing report. Exiting.'); + if (! $report = $input->getOption('report')) { + // @TODO Make selection nicer. + // QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion('Select report:', $reports); + $question + ->setErrorMessage('Report %s does not exist!') + ->setAutocompleterValues($reports); + $report = $helper->ask($input, $output, $question); + $output->writeln('Report ' . $report . ' selected.'); + } + // If parameter option is not recognised check if report name was given. + if (! isset($reports[$report])) { + if (! isset(array_flip($reports)[$report])) { + $output->writeln('Non-existing report ' . $report . '. Exiting.'); return Command::SUCCESS; } - $this->trackingService->setReportId($report); + $report = array_flip($reports)[$report]; } - } - elseif ($report = $this->config->get('tracking_service.youtrack.report.default')) { + $this->trackingService->setReportId($report); + } elseif ($report = $this->config->get('tracking_service.youtrack.report.default')) { $this->trackingService->setReportId($report); } // Currently we only support csv download. - $report_id = $this->trackingService->getReportId(); + $report_id = $this->trackingService->getReportId(); $report_name = $reports[$report_id]; // Code duplication. $cache_clear_status = $this->trackingService->clearReportCache($report_id); @@ -96,7 +106,7 @@ class ReportCommand extends Command { $output->writeln("Csv file downloaded to: {$file}"); } $output->writeln("report: {$report_name}"); - $data = $this->csv->generateReportTable($file); + $data = $this->csv->generateReportTable($file); $table = $this->buildTable($output, $data); $table->render(); @@ -108,13 +118,17 @@ class ReportCommand extends Command { * * @TODO: Code duplication with InvoiceCommand::getTable. */ - protected function buildTable(OutputInterface $output, array $rows): Table { + protected function buildTable(OutputInterface $output, array $rows) : Table + { $table = new Table($output); $table->setHeaders([ - 'Ticket Id', 'Name', 'Time', 'Estimation', + 'Ticket Id', + 'Name', + 'Time', + 'Estimation', ]); foreach ($rows as $key => $row) { - if (!$row) { + if (! $row) { $rows[$key] = new TableSeparator(); } elseif (is_array($row) && is_null($row[0]) && is_null($row[2])) { // Check which elements in array are null. @@ -124,5 +138,4 @@ class ReportCommand extends Command { $table->setRows($rows); return $table; } - } diff --git a/app/src/Commands/TrackCommand.php b/app/src/Commands/TrackCommand.php index 08fae40..1443271 100644 --- a/app/src/Commands/TrackCommand.php +++ b/app/src/Commands/TrackCommand.php @@ -14,8 +14,8 @@ use Symfony\Component\Console\Command\Command; * Later connect this command to the Emacs and have your time tracked directly * from orgmode. */ -class TrackCommand extends Command { - +class TrackCommand extends Command +{ protected $config; protected $youtrack; @@ -25,19 +25,17 @@ class TrackCommand extends Command { YoutrackInterface $youtrack, ?string $name = null ) { - $this->config = $config; + $this->config = $config; $this->youtrack = $youtrack; parent::__construct($name); } - protected function configure(): void { + protected function configure() : void + { $this->setName('youtrack'); $this->setDescription('Track time into your youtrack service'); $this->addUsage('rprt-cli youtrack --issue=[issue-name] --minutes=[minutes] --date=[days-ago] --description=[text] --work-type=[work-type]'); // Options or arguments? Technically they are arguments but default value could be provided by config. // Options are more suitable. } - - - } diff --git a/app/src/Utils/Configuration/TranslationService.php b/app/src/Utils/Configuration/TranslationService.php index e4e587f..997048c 100644 --- a/app/src/Utils/Configuration/TranslationService.php +++ b/app/src/Utils/Configuration/TranslationService.php @@ -4,19 +4,14 @@ declare(strict_types=1); // src/Utils/Configuration/TranslationService.php -use Symfony\Component\Translation\Translator; use RprtCli\Utils\Configuration\ConfigurationInterface; -class TranslationService { - +class TranslationService +{ protected $config; public function __construct(ConfigurationInterface $configuration) { $this->config = $configuration; - } - - - } diff --git a/app/src/Utils/CsvReport/CsvReport.php b/app/src/Utils/CsvReport/CsvReport.php index ae6d388..c05ca58 100644 --- a/app/src/Utils/CsvReport/CsvReport.php +++ b/app/src/Utils/CsvReport/CsvReport.php @@ -10,8 +10,11 @@ use function array_key_first; use function array_keys; use function fgetcsv; use function fopen; +use function is_array; +use function number_format; use function preg_match; use function reset; +use function var_dump; /** * Creates a report of projects and hours. @@ -61,10 +64,9 @@ class CsvReport implements CsvReportInterface /** * Get correct values from the raw data lines of csv. * - * @param array $rawData + * * Columns with data are specified in config. * - * @return array * Project key and unit of time spent. */ protected function parseCsvFile(array $rawData) : array @@ -78,10 +80,11 @@ class CsvReport implements CsvReportInterface return []; } - public function arangeDataForDefaultPdfExport(array $data): array { + public function arangeDataForDefaultPdfExport(array $data) : array + { [$rows, $totalHours, $totalPrice] = [[], 0, 0]; $projectsConfig = $this->configurationService->get('projects'); - $header = $this->configurationService->get('export.labels', null); + $header = $this->configurationService->get('export.labels', null); if (is_array($header)) { $rows[] = $header; } diff --git a/app/src/Utils/CsvReport/CsvReportInterface.php b/app/src/Utils/CsvReport/CsvReportInterface.php index ce33ba4..4db4596 100644 --- a/app/src/Utils/CsvReport/CsvReportInterface.php +++ b/app/src/Utils/CsvReport/CsvReportInterface.php @@ -22,9 +22,8 @@ interface CsvReportInterface /** * Data for default drunomics pdf export. * - * @param array $data + * * Parsed data from csv report. */ - public function arangeDataForDefaultPdfExport(array $data): array; - + public function arangeDataForDefaultPdfExport(array $data) : array; } diff --git a/app/src/Utils/CsvReport/ReportCsv.php b/app/src/Utils/CsvReport/ReportCsv.php index cfc1e7a..befef19 100644 --- a/app/src/Utils/CsvReport/ReportCsv.php +++ b/app/src/Utils/CsvReport/ReportCsv.php @@ -11,17 +11,25 @@ use RprtCli\ValueObjects\WorkInvoiceElementInterface; use function array_key_first; use function array_keys; +use function array_unshift; +use function array_values; +use function explode; use function fgetcsv; use function fopen; +use function implode; +use function is_array; +use function is_numeric; +use function number_format; use function preg_match; use function reset; +use function substr; /** * Creates a report of projects and hours. * * Uses value objects instead of arrays. */ -class ReportCsv implements ReportCsvInterface +class ReportCsv implements ReportCsvInterface { /** * A configuration service. @@ -67,10 +75,9 @@ class ReportCsv implements ReportCsvInterface /** * Get correct values from the raw data lines of csv. * - * @param array $rawData + * * Columns with data are specified in config. * - * @return array * Project key and unit of time spent. */ protected function parseCsvFile(array $rawData) : array @@ -87,9 +94,10 @@ class ReportCsv implements ReportCsvInterface /** * Input is array of Work elements and expenses. */ - public function generateTable(array $data): array { - [$rows, $totalHours, $totalPrice, $add_separator] = [[], 0, 0, FALSE]; - $projectsConfig = $this->configurationService->get('projects'); + public function generateTable(array $data) : array + { + [$rows, $totalHours, $totalPrice, $add_separator] = [[], 0, 0, false]; + $projectsConfig = $this->configurationService->get('projects'); // $header = $this->configurationService->get('export.labels', null); $header = null; if (is_array($header)) { @@ -98,36 +106,36 @@ class ReportCsv implements ReportCsvInterface // First only list work invoice elements. foreach ($data as $key => $invoice_element) { if ($invoice_element instanceof WorkInvoiceElementInterface) { - $add_separator = TRUE; - $project = $invoice_element->getName(); - $time = $invoice_element->getTime(); - $config = $projectsConfig[$project]; - $hours = $config['time_format'] == 'm' ? $time/60 : $time; - $price = $hours * (float) $config['price']; - $row = [ + $add_separator = true; + $project = $invoice_element->getName(); + $time = $invoice_element->getTime(); + $config = $projectsConfig[$project]; + $hours = $config['time_format'] === 'm' ? $time / 60 : $time; + $price = $hours * (float) $config['price']; + $row = [ $config['name'] ?? $project, number_format($hours, 2, ',', '.'), number_format($config['price'], 2, ',', '.'), number_format($price, 2, ',', '.'), ]; - $totalHours += $hours; - $totalPrice += $price; - $rows[] = $row; + $totalHours += $hours; + $totalPrice += $price; + $rows[] = $row; unset($data[$key]); } } if ($add_separator) { // @TODO replace separators with constants for normal separating. - $rows[] = null; - $rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), ' ', number_format($totalPrice, 2, ',', '.')]; - $add_separator = FALSE; + $rows[] = null; + $rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), ' ', number_format($totalPrice, 2, ',', '.')]; + $add_separator = false; } if (empty($data)) { - $add_separator = TRUE; + $add_separator = true; } foreach ($data as $invoice_element) { if ($invoice_element instanceof ExpensesInterface) { - if (!isset($added_expenses)) { + if (! isset($added_expenses)) { // Separator 0: Make next line bold and centered. $rows[] = ReportCsvInterface::SEPARATOR_HARD; $rows[] = [ @@ -137,17 +145,17 @@ class ReportCsv implements ReportCsvInterface 'EUR', ]; // Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml. - $rows[] = ReportCsvInterface::SEPARATOR_SOFT; - $added_expenses = TRUE; + $rows[] = ReportCsvInterface::SEPARATOR_SOFT; + $added_expenses = true; } - $add_separator = TRUE; - $rows[] = [ + $add_separator = true; + $rows[] = [ null, null, $invoice_element->getName(), number_format($invoice_element->getValue(), 2, ',', '.'), ]; - $totalPrice += $invoice_element->getValue(); + $totalPrice += $invoice_element->getValue(); } } if ($add_separator) { @@ -160,19 +168,21 @@ class ReportCsv implements ReportCsvInterface /** * {@inheritdoc} */ - public function arangeDataForDefaultPdfExport(array $data) :array { - $rows = $this->generateTable($data); + public function arangeDataForDefaultPdfExport(array $data) : array + { + $rows = $this->generateTable($data); $header = $this->configurationService->get('export.labels', null); array_unshift($rows, $header); foreach ($rows as $key => $row) { - if (!$row) { + if (! $row) { unset($data[$key]); } } return $rows; } - public function generateReportTable(string $filePath) { + public function generateReportTable(string $filePath) + { // ticket-id, ticket-name, time-spent $data = $this->parseReportData($filePath); if (empty($data)) { @@ -180,51 +190,51 @@ class ReportCsv implements ReportCsvInterface } [$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], []]; foreach ($data as $line) { - $project = explode('-', $line['id'])[0]; + $project = explode('-', $line['id'])[0]; $previous_project = explode('-', $previous)[0]; if ($project !== $previous_project) { // When project changes, add a sum of time for that project. - $table[] = ReportCsvInterface::SEPARATOR_MEDIUM; - $table[] = [null, $previous_project, null, $project_time/60]; - $table[] = ReportCsvInterface::SEPARATOR_MEDIUM; - $time_sum += (float) $project_time; - $project_time = 0; + $table[] = ReportCsvInterface::SEPARATOR_MEDIUM; + $table[] = [null, $previous_project, null, $project_time / 60]; + $table[] = ReportCsvInterface::SEPARATOR_MEDIUM; + $time_sum += (float) $project_time; + $project_time = 0; $all_projects[] = $project; } $project_time += (float) $line['time']; - $previous = $line['id']; - $table[] = array_values($line); + $previous = $line['id']; + $table[] = array_values($line); } // Add sum for the last project. - $table[] = ReportCsvInterface::SEPARATOR_MEDIUM; - $table[] = [null, $project, null, $project_time / 60]; + $table[] = ReportCsvInterface::SEPARATOR_MEDIUM; + $table[] = [null, $project, null, $project_time / 60]; $time_sum += (float) $project_time; // $all_projects[] = $project; // Add a sum of time for whole day. $table[] = ReportCsvInterface::SEPARATOR_MEDIUM; - $table[] = [null, implode(', ', $all_projects), null, $time_sum/60]; + $table[] = [null, implode(', ', $all_projects), null, $time_sum / 60]; return $table; } /** * {@inheritdoc} */ - protected function parseReportData(string $filePath): array + protected function parseReportData(string $filePath) : array { $output = []; // @TODO replace with config service. // $config = $this->dummyConfig()['projects']; if ($file = fopen($filePath, 'r')) { while (($line = fgetcsv($file)) !== false) { - if (!is_numeric($line[4])) { + if (! is_numeric($line[4])) { // Skip header at least. continue; } // @TODO validate line $output[] = [ - 'id' => $line[1], - 'name' => substr($line[2], 0, 60), - 'time' => $line[4], + 'id' => $line[1], + 'name' => substr($line[2], 0, 60), + 'time' => $line[4], 'estimation' => $line[3], ]; } @@ -232,7 +242,6 @@ class ReportCsv implements ReportCsvInterface return $output; } - /** * Should be moved into test class. */ diff --git a/app/src/Utils/CsvReport/ReportCsvInterface.php b/app/src/Utils/CsvReport/ReportCsvInterface.php index 501a29c..2557957 100644 --- a/app/src/Utils/CsvReport/ReportCsvInterface.php +++ b/app/src/Utils/CsvReport/ReportCsvInterface.php @@ -9,16 +9,15 @@ namespace RprtCli\Utils\CsvReport; */ interface ReportCsvInterface { - /** * Normal separator. */ - const SEPARATOR_SOFT = FALSE; + const SEPARATOR_SOFT = false; /** * Medium separator. Next line bold. */ - const SEPARATOR_MEDIUM = NULL; + const SEPARATOR_MEDIUM = null; /** * Next line should be bold and centered. @@ -33,20 +32,20 @@ interface ReportCsvInterface * * Project key as key and number of hours as value. */ - public function getInvoiceData(string $filePath): array; + public function getInvoiceData(string $filePath) : array; /** * Returns array of rows created from array of InvoiceElements. * * If row is null, it is meant to be a table separator. */ - public function generateTable(array $data) :array; + public function generateTable(array $data) : array; /** * Data for default drunomics pdf export. * - * @param array $data + * * Parsed data from csv report. */ - public function arangeDataForDefaultPdfExport(array $data): array; + public function arangeDataForDefaultPdfExport(array $data) : array; } diff --git a/app/src/Utils/Mailer/MailerInterface.php b/app/src/Utils/Mailer/MailerInterface.php index 4361b7b..158e70d 100644 --- a/app/src/Utils/Mailer/MailerInterface.php +++ b/app/src/Utils/Mailer/MailerInterface.php @@ -7,6 +7,6 @@ namespace RprtCli\Utils\Mailer; /** * Methods for symfony (swift)mailer service. */ -interface MailerInterface { - +interface MailerInterface +{ } diff --git a/app/src/Utils/Mailer/MailerService.php b/app/src/Utils/Mailer/MailerService.php index 82341bd..72b3380 100644 --- a/app/src/Utils/Mailer/MailerService.php +++ b/app/src/Utils/Mailer/MailerService.php @@ -6,21 +6,33 @@ declare(strict_types=1); namespace RprtCli\Utils\Mailer; +use Exception; use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\PdfExport\PdfExportInterface; -use Symfony\Component\Mime\Email; -use \Exception; -use Symfony\Component\Mailer\Transport; use Symfony\Component\Mailer\Mailer; +use Symfony\Component\Mailer\Transport; +use Symfony\Component\Mime\Email; + +use function date; +use function explode; +use function fgets; +use function file_exists; +use function rawurlencode; +use function readline; +use function strtotime; +use function system; +use function trim; +use function var_dump; + +use const STDIN; /** * Send emails with invoices as attachments. * * https://symfony.com/doc/current/mailer.html */ -class MailerService implements MailerInterface { - - +class MailerService implements MailerInterface +{ protected $config; protected $pdf; @@ -34,31 +46,35 @@ class MailerService implements MailerInterface { protected $email; - public function __construct(ConfigurationInterface $config, PdfExportInterface $pdf) { + public function __construct(ConfigurationInterface $config, PdfExportInterface $pdf) + { $this->config = $config; - $this->pdf = $pdf; + $this->pdf = $pdf; } - public function setRecipients(array $to): void + public function setRecipients(array $to) : void { $this->to = $to; } - public function setSubject(string $subject): void { + public function setSubject(string $subject) : void + { $this->subject = $subject; } - public function setAttachment(string $path): void { + public function setAttachment(string $path) : void + { // @TODO - add some error handling. $this->attachment = $path; } - public function getProperty(string $property) { + public function getProperty(string $property) + { // Only for simple value properies - string and numbers. // from, to, subject. - if (!isset($this->{$property})) { - $value = $this->config->get('email.' . $property, FALSE); - if (!$value) { + if (! isset($this->{$property})) { + $value = $this->config->get('email.' . $property, false); + if (! $value) { $value = readline("Property {$property} is not configured. Enter value: "); } $this->{$property} = $value; @@ -66,10 +82,11 @@ class MailerService implements MailerInterface { return $this->{$property}; } - protected function getRecipients() { - if (!isset($this->to)) { - $value = $this->config->get('email.to', FALSE); - if (!$value) { + protected function getRecipients() + { + if (! isset($this->to)) { + $value = $this->config->get('email.to', false); + if (! $value) { $value = explode(',', readline('Provide recipients\' emails separated by a comma: ')); } $this->to = $value; @@ -77,7 +94,8 @@ class MailerService implements MailerInterface { return $this->to; } - private function readPassword($prompt = "Enter Password:") { + private function readPassword($prompt = "Enter Password:") + { echo $prompt; system('stty -echo'); $password = trim(fgets(STDIN)); @@ -85,10 +103,11 @@ class MailerService implements MailerInterface { return $password; } - protected function getPasswordProperty() { - if (!isset($this->password)) { - $value = $this->config->get('email.password', FALSE); - if (!$value) { + protected function getPasswordProperty() + { + if (! isset($this->password)) { + $value = $this->config->get('email.password', false); + if (! $value) { $value = $this->readPassword(); } $this->password = $value; @@ -96,8 +115,8 @@ class MailerService implements MailerInterface { return $this->password; } - - public function sendMail(string $from, array $to, string $subject, string $text, array $attachment = []): void { + public function sendMail(string $from, array $to, string $subject, string $text, array $attachment = []) : void + { $email = new Email(); $email->from($from); $email->to(...$to); @@ -105,21 +124,21 @@ class MailerService implements MailerInterface { // https://github.com/symfony/mailer $email->subject($subject); $email->text($text); - if (!empty($attachment)) { - if (!isset($attachment['path'])) { + if (! empty($attachment)) { + if (! isset($attachment['path'])) { var_dump('Attachment path missing!'); - } - else { + } else { $email->attachFromPath($attachment['path'], $attachment['name'] ?? null, $attachment['type'] ?? null); } } // Not sure whether it would be nicer to use class property instead of variable. $transport = $this->getTransport(); - $mailer = $this->getMailer($transport); + $mailer = $this->getMailer($transport); $mailer->send($email); } - public function getTransport() { + public function getTransport() + { // @TODO remove username and password from config. $username = rawurlencode($this->getProperty('username')); $password = rawurlencode($this->getPasswordProperty()); @@ -129,13 +148,15 @@ class MailerService implements MailerInterface { return Transport::fromDsn($mailer_dsn); } - public function getMailer($transport) { + public function getMailer($transport) + { return new Mailer($transport); } - public function sendDefaultMail(string $output): void { - $tokens = $this->pdf->gatherTokensForTemplate($this->getEmailTemplatePath(), FALSE, $this->getDefaultTokens(), 'email.tokens'); - $text = $this->pdf->replaceTokensInTemplate($this->getEmailTemplatePath(), $tokens); + public function sendDefaultMail(string $output) : void + { + $tokens = $this->pdf->gatherTokensForTemplate($this->getEmailTemplatePath(), false, $this->getDefaultTokens(), 'email.tokens'); + $text = $this->pdf->replaceTokensInTemplate($this->getEmailTemplatePath(), $tokens); $this->sendMail( $this->getProperty('from'), $this->getProperty('to'), @@ -145,21 +166,23 @@ class MailerService implements MailerInterface { ); } - public function getDefaultTokens(): array { - $tokens = []; - $date = strtotime('-1 month'); + public function getDefaultTokens() : array + { + $tokens = []; + $date = strtotime('-1 month'); $tokens['month'] = date('F', $date); - $tokens['year'] = date('Y', $date); + $tokens['year'] = date('Y', $date); return $tokens; } - protected function getEmailTemplatePath(): ?string { - if (!isset($this->templatePath)) { - $template_path = $this->config->get('email.template_path', FALSE); - if (!$template_path) { + protected function getEmailTemplatePath() : ?string + { + if (! isset($this->templatePath)) { + $template_path = $this->config->get('email.template_path', false); + if (! $template_path) { $template_path = readline('Enter template file path: '); } - if (!file_exists($template_path)) { + if (! file_exists($template_path)) { throw new Exception('Template file not found!'); } $this->templatePath = $template_path; @@ -167,12 +190,12 @@ class MailerService implements MailerInterface { return $this->templatePath; } - public function setEmailTemplatePath(string $path): void { + public function setEmailTemplatePath(string $path) : void + { if (file_exists($path)) { $this->templatePath = $path; return; } throw new Exception('Email template file not found!'); } - } diff --git a/app/src/Utils/PdfExport/PdfExportInterface.php b/app/src/Utils/PdfExport/PdfExportInterface.php index 2e9026c..04c2e86 100644 --- a/app/src/Utils/PdfExport/PdfExportInterface.php +++ b/app/src/Utils/PdfExport/PdfExportInterface.php @@ -7,36 +7,35 @@ namespace RprtCli\Utils\PdfExport; /** * Handles exporting parsed csv data to pdf files. */ -interface PdfExportInterface { - +interface PdfExportInterface +{ /** * Retrieves path to template file either from command option, configuration * or from uer input. */ - public function getTemplatePath(): ?string; + public function getTemplatePath() : ?string; - public function setTemplatePath(string $path): void; + public function setTemplatePath(string $path) : void; /** * Creates html table from parsed csv data. * - * @param array $data + * * First line is header. The rest of them is table body. */ - public function parsedDataToHtmlTable(array $data): ?string; + public function parsedDataToHtmlTable(array $data) : ?string; /** * Reads the template file and replaces token values. */ - public function replaceTokensInTemplate(string $template_path, array $tokens): ?string; + public function replaceTokensInTemplate(string $template_path, array $tokens) : ?string; /** * Creates and export file. * - * @param string $html + * * Template file with tokens replaced. * - * @return bool * True if export was successfull. */ public function pdfExport(string $html) : bool; @@ -44,39 +43,37 @@ interface PdfExportInterface { /** * Goes through the whole process of creating a pdf for the invoice. * - * @param array $nice_data + * * Parsed csv report export data. * - * @return string * Path of the pdf file. */ - public function fromDefaultDataToPdf(array $nice_data, array $tokens = []): string; + public function fromDefaultDataToPdf(array $nice_data, array $tokens = []) : string; /** * Sets output file override via command paramater. * - * @param string $output + * * Path of the output pdf file ('/tmp/test.pdf'). */ - public function setOutput(string $output): void; + public function setOutput(string $output) : void; // @TODO support multiple templates by adding template id in config. // @TODO implement twig. /** * Get tokens to replace in the template. * - * @param string $template_path + * * Path to the template file (long term plan is to support multiple templates). - * @param bool $skip_missing + * + * * Just skip missing tokens. - * @param array $runtime_tokens + * * Provide tokens at runtime of the application (not supported yet). @TODO - * @param string $config + * * Config path to tokens. * - * @return array * Token keys and values array. */ - public function gatherTokensForTemplate(string $template_path, bool $skip_missing, array $runtime_tokens = [], string $config = 'export.token'): array; - + public function gatherTokensForTemplate(string $template_path, bool $skip_missing, array $runtime_tokens = [], string $config = 'export.token') : array; } diff --git a/app/src/Utils/PdfExport/PdfExportService.php b/app/src/Utils/PdfExport/PdfExportService.php index d1eec6d..0972825 100644 --- a/app/src/Utils/PdfExport/PdfExportService.php +++ b/app/src/Utils/PdfExport/PdfExportService.php @@ -5,12 +5,23 @@ declare(strict_types=1); namespace RprtCli\Utils\PdfExport; use Exception; -use RprtCli\Utils\Configuration\ConfigurationInterface; use Mpdf\Output\Destination; +use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\CsvReport\ReportCsvInterface; -class PdfExportService implements PdfExportInterface { +use function array_shift; +use function date; +use function file_exists; +use function file_get_contents; +use function implode; +use function mktime; +use function preg_match_all; +use function readline; +use function str_replace; +use function strtotime; +class PdfExportService implements PdfExportInterface +{ protected $templatePath; protected $output; @@ -18,18 +29,20 @@ class PdfExportService implements PdfExportInterface { protected $mpdf; - public function __construct(ConfigurationInterface $config, $mpdf) { + public function __construct(ConfigurationInterface $config, $mpdf) + { $this->config = $config; - $this->mpdf = $mpdf; + $this->mpdf = $mpdf; } - public function getTemplatePath(): ?string { - if (!isset($this->templatePath)) { - $template_path = $this->config->get('export.template_path', FALSE); - if (!$template_path) { + public function getTemplatePath() : ?string + { + if (! isset($this->templatePath)) { + $template_path = $this->config->get('export.template_path', false); + if (! $template_path) { $template_path = readline('Enter template file path: '); } - if (!file_exists($template_path)) { + if (! file_exists($template_path)) { throw new Exception('Template file not found!'); } $this->templatePath = $template_path; @@ -37,7 +50,8 @@ class PdfExportService implements PdfExportInterface { return $this->templatePath; } - public function setTemplatePath(string $path): void { + public function setTemplatePath(string $path) : void + { if (file_exists($path)) { $this->templatePath = $path; return; @@ -50,40 +64,39 @@ class PdfExportService implements PdfExportInterface { // @TODO would it make sense to allow more per user extending? // @TODO - too much assumptions on data structure. Create a class with // precise data structure and use that to pass the data around. - public function parsedDataToHtmlTable(array $data): ?string { - $table = ''; - $header = array_shift($data); + public function parsedDataToHtmlTable(array $data) : ?string + { + $table = '
'; + $header = array_shift($data); $classes = ''; foreach ($header as $index => $cell) { $table .= ""; } $table .= ''; foreach ($data as $row_index => $row) { - if (!$row) { + if (! $row) { if ($row === ReportCsvInterface::SEPARATOR_MEDIUM) { $classes = 'bold'; - } - elseif ($row === ReportCsvInterface::SEPARATOR_HARD) { + } elseif ($row === ReportCsvInterface::SEPARATOR_HARD) { $classes = 'bold center'; } continue; } - list($cells, $colspan) = [[], 0]; + [$cells, $colspan] = [[], 0]; foreach ($row as $index => $cell) { - if (!$cell) { + if (! $cell) { $colspan += 1; continue; } if ($colspan) { $colspan += 1; - $cells[] = ""; - $colspan = 0; - } - else { + $cells[] = ""; + $colspan = 0; + } else { $cells[] = ""; } } - $rows[] = "" . implode($cells) . ''; + $rows[] = "" . implode($cells) . ''; $classes = ''; } $table .= implode('', $rows); @@ -92,7 +105,7 @@ class PdfExportService implements PdfExportInterface { return $table; } - public function replaceTokensInTemplate(string $template_path, array $tokens): ?string + public function replaceTokensInTemplate(string $template_path, array $tokens) : ?string { $template = file_get_contents($template_path); foreach ($tokens as $key => $value) { @@ -102,7 +115,8 @@ class PdfExportService implements PdfExportInterface { } // @TODO write a method to gather tokens. - public function getTokensInTemplate(string $template): array { + public function getTokensInTemplate(string $template) : array + { // @TODO find substrings of type [[key]] preg_match_all('/\[\[([a-z0-9-_]+)\]\]/', $template, $match); return $match[1]; @@ -112,41 +126,40 @@ class PdfExportService implements PdfExportInterface { /** * Get tokens to replace in the template. * - * @param string $template_path + * * Path to the template file (long term plan is to support multiple templates). - * @param bool $skip_missing + * + * * Just skip missing tokens. - * @param array $runtime_tokens + * * Provide tokens at runtime of the application (not supported yet). @TODO - * @param string $config + * * Config path to tokens. * - * @return array * Token keys and values array. */ - public function gatherTokensForTemplate(string $template_path, bool $skip_missing = FALSE, array $runtime_tokens = [], string $config = 'export.tokens'): array { - list($tokens, $missing) = [[], []]; - $token_keys = $this->getTokensInTemplate(file_get_contents($template_path)); - $config_tokens = $this->config->get($config); + public function gatherTokensForTemplate(string $template_path, bool $skip_missing = false, array $runtime_tokens = [], string $config = 'export.tokens') : array + { + [$tokens, $missing] = [[], []]; + $token_keys = $this->getTokensInTemplate(file_get_contents($template_path)); + $config_tokens = $this->config->get($config); foreach ($token_keys as $token_key) { if (isset($runtime_tokens[$token_key])) { $tokens[$token_key] = $runtime_tokens[$token_key]; - } - elseif (!isset($config_tokens[$token_key]) && !$skip_missing) { + } elseif (! isset($config_tokens[$token_key]) && ! $skip_missing) { $tokens[$token_key] = readline("Enter value to replace [[{$token_key}]] in template: "); - } - elseif (isset($config_tokens[$token_key])) { + } elseif (isset($config_tokens[$token_key])) { $tokens[$token_key] = $config_tokens[$token_key]; - } - else { + } else { $missing[] = $token_key; } } return $tokens; } - public function pdfExport(string $html, $output = null): bool { - $this->mpdf->SetProtection(array('print')); + public function pdfExport(string $html, $output = null) : bool + { + $this->mpdf->SetProtection(['print']); // @TODO make configurable. $this->mpdf->SetTitle("Invoice"); $this->mpdf->SetAuthor("rprt-cli"); @@ -156,29 +169,32 @@ class PdfExportService implements PdfExportInterface { return file_exists($this->output); } - protected function getOutput() { + protected function getOutput() + { if (isset($this->output)) { return $this->output; } - $output = $this->config->get('export.output', NULL) ?? readline('Enter output file path: '); - $date = strtotime("-1 month"); - $output = str_replace('[[month]]', date('F', $date), $output); - $output = str_replace('[[year]]', date('Y', $date), $output); + $output = $this->config->get('export.output', null) ?? readline('Enter output file path: '); + $date = strtotime("-1 month"); + $output = str_replace('[[month]]', date('F', $date), $output); + $output = str_replace('[[year]]', date('Y', $date), $output); $this->output = $output; return $output; } - public function setOutput(string $path): void { + public function setOutput(string $path) : void + { $this->output = $path; } - public function fromDefaultDataToPdf(array $data, array $tokens = []): string { - $template_path = $this->getTemplatePath(); - $tokens = $this->defaultTokens(); + public function fromDefaultDataToPdf(array $data, array $tokens = []) : string + { + $template_path = $this->getTemplatePath(); + $tokens = $this->defaultTokens(); $tokens['table'] = $this->parsedDataToHtmlTable($data); - $tokens = $this->gatherTokensForTemplate($template_path, FALSE, $tokens); - $html = $this->replaceTokensInTemplate($template_path, $tokens); - $success = $this->pdfExport($html); + $tokens = $this->gatherTokensForTemplate($template_path, false, $tokens); + $html = $this->replaceTokensInTemplate($template_path, $tokens); + $success = $this->pdfExport($html); if ($success) { return $this->getOutput(); } @@ -188,14 +204,14 @@ class PdfExportService implements PdfExportInterface { /** * Get default tokens. */ - protected function defaultTokens(): array { - $tokens = []; - $tokens['today'] = date('j. m. y'); - $month_ago = strtotime('1 month ago'); - $tokens['number'] = date('m/y', $month_ago); + protected function defaultTokens() : array + { + $tokens = []; + $tokens['today'] = date('j. m. y'); + $month_ago = strtotime('1 month ago'); + $tokens['number'] = date('m/y', $month_ago); $tokens['date_start'] = date('1. m. Y', $month_ago); - $tokens['date_end'] = date("d. m. Y", mktime(0, 0, 0, (int) date("m"), 0)); + $tokens['date_end'] = date("d. m. Y", mktime(0, 0, 0, (int) date("m"), 0)); return $tokens; } - } diff --git a/app/src/Utils/TimeTrackingServices/YoutrackInterface.php b/app/src/Utils/TimeTrackingServices/YoutrackInterface.php index e04b5cd..5230e47 100644 --- a/app/src/Utils/TimeTrackingServices/YoutrackInterface.php +++ b/app/src/Utils/TimeTrackingServices/YoutrackInterface.php @@ -19,12 +19,19 @@ interface YoutrackInterface /** * Downloads report and returns file path. * - * @param string $report_id + * * Youtrack internal report id. * - * @return NULL|string + * * If fetch was unsuccssefull return false, otherwise the file path. */ public function downloadReport(string $report_id) : ?string; + /** + * Get a list of reports. + * + * + * Array of reports with ids as keys and names as values. + */ + public function listReports() : array; } diff --git a/app/src/Utils/TimeTrackingServices/YoutrackService.php b/app/src/Utils/TimeTrackingServices/YoutrackService.php index 22be3ec..9b3b992 100644 --- a/app/src/Utils/TimeTrackingServices/YoutrackService.php +++ b/app/src/Utils/TimeTrackingServices/YoutrackService.php @@ -6,13 +6,28 @@ declare(strict_types=1); namespace RprtCli\Utils\TimeTrackingServices; +use Exception; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\ClientException; use RprtCli\Utils\Configuration\ConfigurationInterface; +use Throwable; + +use function array_combine; +use function array_filter; +use function array_map; +use function file_put_contents; +use function floor; +use function json_decode; +use function microtime; +use function readline; +use function sleep; +use function sys_get_temp_dir; +use function tempnam; +use function trim; +use function var_dump; class YoutrackService implements YoutrackInterface { - protected $ytToken; protected $ytBaseUrl; @@ -24,45 +39,47 @@ class YoutrackService implements YoutrackInterface public function __construct(ConfigurationInterface $config, ClientInterface $http_client) { - $this->config = $config; + $this->config = $config; $this->httpClient = $http_client; } - public function testYoutrackapi(): ?string + public function testYoutrackapi() : ?string { // Get base url from config or add input. // Get token or add input. - $path = 'youtrack/api/admin/users/me'; - $yt_url = $this->getYtUrl($path); - $yt_token = $this->getYtToken(); - $query = ['fields' => 'id,email,fullName']; - $headers = [ + $path = 'youtrack/api/admin/users/me'; + $yt_url = $this->getYtUrl($path); + $yt_token = $this->getYtToken(); + $query = ['fields' => 'id,email,fullName']; + $headers = [ "Authorization" => "Bearer $yt_token", - 'Cache-Control' =>'no-cache', + 'Cache-Control' => 'no-cache', ]; $me_response = $this->httpClient->request('GET', $yt_url, [ - 'query' => $query, - 'headers' => $headers + 'query' => $query, + 'headers' => $headers, ]); - $test = (string) $me_response->getBody()->getContents(); - $me_json = (array) json_decode($test); + $test = (string) $me_response->getBody()->getContents(); + $me_json = (array) json_decode($test); if ($me_json && isset($me_json['fullName'])) { return $me_json['fullName']; } - return NULL; + return null; } - protected function requestYoutrackPath(string $path, array $query) { - $yt_url = $this->getYtUrl($path); + protected function requestYoutrackPath(string $path, array $query) + { + $yt_url = $this->getYtUrl($path); $headers = $this->getHeaders(); return $this->httpClient->request('GET', $yt_url, [ - 'query' => $query, - 'headers' => $headers + 'query' => $query, + 'headers' => $headers, ]); } - protected function getHeaders() { + protected function getHeaders() + { $yt_token = $this->getYtToken(); return [ "Authorization" => "Bearer $yt_token", @@ -70,7 +87,7 @@ class YoutrackService implements YoutrackInterface ]; } - public function getReportId(): ?string + public function getReportId() : ?string { // --report option value should take precedence. // @TODO error handling. @@ -78,132 +95,135 @@ class YoutrackService implements YoutrackInterface return $this->report_id; } $yt_report_id = $this->config->get('tracking_service.youtrack.report_id'); - if (!$yt_report_id) { + if (! $yt_report_id) { $yt_report_id = readline('Enter the report id: '); } return $yt_report_id; } - public function setReportId(string $report_id) :void { + public function setReportId(string $report_id) : void + { $this->report_id = $report_id; } - public function downloadReport(string $report_id): ?string + public function downloadReport(string $report_id) : ?string { - $path = "youtrack/api/reports/$report_id/export/csv"; - $query = ['$top' => -1]; + $path = "youtrack/api/reports/$report_id/export/csv"; + $query = ['$top' => -1]; $yt_token = $this->getYtToken(); - $headers = [ + $headers = [ 'Accept' => 'application/json, text/plain, */*', // 'Accept-Encoding' => 'gzip, deflate, br', // 'Connection' => 'keep-alive', 'Accept-Language' => 'en-US,en;q=0.5', - "Authorization" => "Bearer $yt_token", + "Authorization" => "Bearer $yt_token", ]; try { $csv_response = $this->httpClient->request('GET', $this->getYtUrl($path), [ 'headers' => $headers, - 'query' => $query, + 'query' => $query, ]); // Write csv response test into temporary file. $csv_file = tempnam(sys_get_temp_dir(), "rprt-csv-{$report_id}"); file_put_contents($csv_file, $csv_response->getBody()); return $csv_file; - } - catch (ClientException $e) { + } catch (ClientException $e) { $status = $e->getResponse()->getStatusCode(); - if ($status == 409) { + if ($status === 409) { sleep(3); // @TODO Find a way to break of of loop if necessary! var_dump("409 response status during download of report {$report_id}. Sleep 3 and try again."); return $this->downloadReport($report_id); } - } - catch (\Throwable $t) { + } catch (Throwable $t) { $status = $t->getMessage(); var_dump($status); } - throw new \Exception("Unable to download report {$report_id}!"); + throw new Exception("Unable to download report {$report_id}!"); } - protected function getYtToken(): string { + protected function getYtToken() : string + { if (isset($this->ytToken)) { return $this->ytToken; } - $yt_token = $this->config->get('tracking_service.youtrack.auth_token', FALSE); - if (!$yt_token) { + $yt_token = $this->config->get('tracking_service.youtrack.auth_token', false); + if (! $yt_token) { $yt_token = readline('Enter your youtrack authentication token: '); } return $yt_token; } - public function setYtToken(string $token): void { + public function setYtToken(string $token) : void + { $this->ytToken = $token; } - protected function getYtUrl(string $path = ''): ?string { + protected function getYtUrl(string $path = '') : ?string + { if (isset($this->ytBaseUrl)) { $yt_base_url = $this->ytBaseUrl; - } - else { - $yt_base_url = $this->config->get('tracking_service.youtrack.base_url', FALSE); + } else { + $yt_base_url = $this->config->get('tracking_service.youtrack.base_url', false); } if (empty($yt_base_url)) { $yt_base_url = readline('Enter base url for of the youtrack service: '); } - if (!empty($path)) { - $yt_base_url = $yt_base_url . '/' . trim($path, '/'); + if (! empty($path)) { + $yt_base_url .= '/' . trim($path, '/'); } return $yt_base_url; } - public function setYtUrl(string $base_url) { + public function setYtUrl(string $base_url) + { $this->ytBaseUrl = $base_url; } - public function listReports() { + public function listReports() : array + { // Now filter results by own = true; - $url = '/youtrack/api/reports'; - $query = [ - '$top' => -1, + $url = '/youtrack/api/reports'; + $query = [ + '$top' => -1, 'fields' => 'id,name,own,owner(login,name)', ]; - $response = $this->requestYoutrackPath($url, $query); - $body = (string) $response->getBody()->getContents(); - $body_json = (array) json_decode($body); + $response = $this->requestYoutrackPath($url, $query); + $body = (string) $response->getBody()->getContents(); + $body_json = (array) json_decode($body); $my_reports = array_filter($body_json, fn($report) => $report->own); - $reports = array_combine( + $reports = array_combine( array_map(fn($r) => $r->id, $my_reports), array_map(fn ($r) => $r->name, $my_reports) ); return $reports; } - public function clearReportCache(string $report_id) :int { - $path = "/youtrack/api/reports/${report_id}/status"; - $query = [ - '$top' => -1, - 'fields' => 'calculationInProgress,error(id),errorMessage,isOutdated,lastCalculated,progress,wikifiedErrorMessage' + public function clearReportCache(string $report_id) : int + { + $path = "/youtrack/api/reports/${report_id}/status"; + $query = [ + '$top' => -1, + 'fields' => 'calculationInProgress,error(id),errorMessage,isOutdated,lastCalculated,progress,wikifiedErrorMessage', ]; - $post = [ - 'lastCalculated' => floor(microtime(true) * 1000), + $post = [ + 'lastCalculated' => floor(microtime(true) * 1000), 'calculationInProgress' => true, - 'wikifiedErrorMessage' => '', - 'isOutdated' => false, - 'progress' => -1, - 'error' => null, - 'errorMessage' => null, - '$type' => 'ReportStatus' + 'wikifiedErrorMessage' => '', + 'isOutdated' => false, + 'progress' => -1, + 'error' => null, + 'errorMessage' => null, + '$type' => 'ReportStatus', ]; - $yt_url = $this->getYtUrl($path); + $yt_url = $this->getYtUrl($path); $response = $this->httpClient->request('POST', $yt_url, [ - 'query' => $query, + 'query' => $query, 'headers' => $this->getHeaders(), - 'json' => $post, + 'json' => $post, ]); // $body = (string) $response->getBody()->getContents(); // var_dump($body); return $response->getStatusCode(); } - } diff --git a/app/src/ValueObjects/Expenses.php b/app/src/ValueObjects/Expenses.php index 5697635..e3398c9 100644 --- a/app/src/ValueObjects/Expenses.php +++ b/app/src/ValueObjects/Expenses.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace RprtCli\ValueObjects; -class Expenses implements ExpensesInterface { - +class Expenses implements ExpensesInterface +{ /** * Expenses in current currency. */ @@ -16,17 +16,18 @@ class Expenses implements ExpensesInterface { */ private string $name; - public function __construct(string $name, float $value) { - $this->name = $name; + public function __construct(string $name, float $value) + { + $this->name = $name; $this->value = $value; } - public function getValue(): float + public function getValue() : float { return $this->value; } - public function getName(): string + public function getName() : string { return $this->name; } diff --git a/app/src/ValueObjects/ExpensesInterface.php b/app/src/ValueObjects/ExpensesInterface.php index 6f90ed2..2985be2 100644 --- a/app/src/ValueObjects/ExpensesInterface.php +++ b/app/src/ValueObjects/ExpensesInterface.php @@ -4,8 +4,7 @@ declare(strict_types=1); namespace RprtCli\ValueObjects; -interface ExpensesInterface extends InvoiceElementInterface { - - public function getValue() :float; - +interface ExpensesInterface extends InvoiceElementInterface +{ + public function getValue() : float; } diff --git a/app/src/ValueObjects/InvoiceElementInterface.php b/app/src/ValueObjects/InvoiceElementInterface.php index 36f182a..00c3e77 100644 --- a/app/src/ValueObjects/InvoiceElementInterface.php +++ b/app/src/ValueObjects/InvoiceElementInterface.php @@ -7,11 +7,10 @@ namespace RprtCli\ValueObjects; /** * Main interface for invoice elements. */ -interface InvoiceElementInterface { - +interface InvoiceElementInterface +{ /** * Project or expenses name. */ - public function getName() :string; - + public function getName() : string; } diff --git a/app/src/ValueObjects/WorkInvoiceElement.php b/app/src/ValueObjects/WorkInvoiceElement.php index dce672e..54383e0 100644 --- a/app/src/ValueObjects/WorkInvoiceElement.php +++ b/app/src/ValueObjects/WorkInvoiceElement.php @@ -4,8 +4,8 @@ declare(strict_types=1); namespace RprtCli\ValueObjects; -class WorkInvoiceElement implements WorkInvoiceElementInterface { - +class WorkInvoiceElement implements WorkInvoiceElementInterface +{ private float $time; /** @@ -13,17 +13,19 @@ class WorkInvoiceElement implements WorkInvoiceElementInterface { */ private string $name; - public function __construct(string $name, float $time) { + public function __construct(string $name, float $time) + { $this->name = $name; $this->time = $time; } - public function getTime() :float { + public function getTime() : float + { return $this->time; } - public function getName() :string { + public function getName() : string + { return $this->name; } - } diff --git a/app/src/ValueObjects/WorkInvoiceElementInterface.php b/app/src/ValueObjects/WorkInvoiceElementInterface.php index 83b4c3a..321d4bb 100644 --- a/app/src/ValueObjects/WorkInvoiceElementInterface.php +++ b/app/src/ValueObjects/WorkInvoiceElementInterface.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace RprtCli\ValueObjects; -interface WorkInvoiceElementInterface extends InvoiceElementInterface { - - public function getTime() :float ; +interface WorkInvoiceElementInterface extends InvoiceElementInterface +{ + public function getTime() : float; /** * Get project name. */ - public function getName() :string ; + public function getName() : string; }
{$cell}
{$cell}{$cell}{$cell}