Install code sniffer and run code beautifier.

yt-rest-api
Lio Novelli 2022-12-20 23:36:07 +01:00
parent cfc623ba72
commit 4dcbcb228a
21 changed files with 558 additions and 388 deletions

View File

@ -28,12 +28,18 @@
} }
}, },
"require-dev": { "require-dev": {
"squizlabs/php_codesniffer": "^3.5", "squizlabs/php_codesniffer": "^3.7",
"phpunit/phpunit": "^9.5", "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": { "scripts": {
"cs": "phpcs", "cs": "phpcs --colors --standard=PSR12",
"cbf": "phpcbf" "cbf": "phpcbf"
},
"config": {
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
} }
} }

81
app/composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "81050635de2f87c3f7f693ec8cb30645", "content-hash": "7b0be09c8f282dfcceb075d1455caa28",
"packages": [ "packages": [
{ {
"name": "doctrine/lexer", "name": "doctrine/lexer",
@ -2949,6 +2949,68 @@
}, },
"time": "2022-02-21T01:04:05+00:00" "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", "name": "phpdocumentor/reflection-common",
"version": "2.2.0", "version": "2.2.0",
@ -4659,16 +4721,16 @@
}, },
{ {
"name": "squizlabs/php_codesniffer", "name": "squizlabs/php_codesniffer",
"version": "3.5.8", "version": "3.7.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
"reference": "9d583721a7157ee997f235f327de038e7ea6dac4" "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/9d583721a7157ee997f235f327de038e7ea6dac4", "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
"reference": "9d583721a7157ee997f235f327de038e7ea6dac4", "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -4706,7 +4768,12 @@
"phpcs", "phpcs",
"standards" "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", "name": "theseer/tokenizer",
@ -4869,5 +4936,5 @@
"prefer-lowest": false, "prefer-lowest": false,
"platform": [], "platform": [],
"platform-dev": [], "platform-dev": [],
"plugin-api-version": "2.1.0" "plugin-api-version": "2.3.0"
} }

View File

@ -6,6 +6,7 @@ declare(strict_types=1);
namespace RprtCli\Commands; namespace RprtCli\Commands;
use Exception;
use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\Utils\CsvReport\ReportCsvInterface; use RprtCli\Utils\CsvReport\ReportCsvInterface;
use RprtCli\Utils\Mailer\MailerInterface; use RprtCli\Utils\Mailer\MailerInterface;
@ -15,14 +16,24 @@ use RprtCli\ValueObjects\Expenses;
use RprtCli\ValueObjects\WorkInvoiceElement; use RprtCli\ValueObjects\WorkInvoiceElement;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; 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_dump;
use function var_export;
// use Symfony\Contracts\Translation\TranslatorInterface;
/** /**
* Main file - invoice command. * Main file - invoice command.
@ -37,7 +48,7 @@ class InvoiceCommand extends Command
protected $pdfExport; protected $pdfExport;
const TYPE_WORK = 1; const TYPE_WORK = 1;
const TYPE_EXPENSE = 2; const TYPE_EXPENSE = 2;
public function __construct( public function __construct(
@ -51,8 +62,8 @@ class InvoiceCommand extends Command
$this->csv = $csv; $this->csv = $csv;
$this->configuration = $configuration; $this->configuration = $configuration;
$this->youtrack = $youtrack; $this->youtrack = $youtrack;
$this->pdfExport = $pdf_export; $this->pdfExport = $pdf_export;
$this->mailer = $mailer; $this->mailer = $mailer;
parent::__construct($name); parent::__construct($name);
} }
@ -112,14 +123,14 @@ class InvoiceCommand extends Command
'e', 'e',
InputOption::VALUE_OPTIONAL, InputOption::VALUE_OPTIONAL,
'List of additional expenses in format expense1=value1;expenses2=value2... or empty for interactive output.', 'List of additional expenses in format expense1=value1;expenses2=value2... or empty for interactive output.',
FALSE false
); );
$this->addOption( $this->addOption(
'custom', 'custom',
'c', 'c',
InputOption::VALUE_OPTIONAL, 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.', '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( $this->addOption(
'list-reports', 'list-reports',
@ -132,7 +143,7 @@ class InvoiceCommand extends Command
'r', 'r',
InputOption::VALUE_OPTIONAL, InputOption::VALUE_OPTIONAL,
'Show time tracked for report.', 'Show time tracked for report.',
FALSE false
); );
} }
@ -144,16 +155,15 @@ class InvoiceCommand extends Command
} }
if ($input->getOption('list-reports')) { if ($input->getOption('list-reports')) {
$list = $this->youtrack->listReports(); $list = $this->youtrack->listReports();
$output->writeln(var_export($list, TRUE)); $output->writeln(var_export($list, true));
return Command::SUCCESS; return Command::SUCCESS;
} }
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) { if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
if ($report = $input->getOption('report')) { if ($report = $input->getOption('report')) {
$this->youtrack->setReportId($report); $this->youtrack->setReportId($report);
} } else {
else {
$reports = $this->youtrack->listReports(); $reports = $this->youtrack->listReports();
$count = 1; $count = 1;
foreach ($reports as $id => $name) { foreach ($reports as $id => $name) {
$output->writeln("[{$count}] {$name} ({$id})"); $output->writeln("[{$count}] {$name} ({$id})");
$count++; $count++;
@ -167,7 +177,7 @@ class InvoiceCommand extends Command
} }
} }
if ($youtrack = $input->getOption('youtrack')) { if ($youtrack = $input->getOption('youtrack')) {
$report_id = $this->youtrack->getReportId(); $report_id = $this->youtrack->getReportId();
$cache_clear_status = $this->youtrack->clearReportCache($report_id); $cache_clear_status = $this->youtrack->clearReportCache($report_id);
if ($output->isVerbose()) { if ($output->isVerbose()) {
$output->writeln("Report <info>{$report_id}</info> cache cleared, status: {$cache_clear_status}"); $output->writeln("Report <info>{$report_id}</info> cache cleared, status: {$cache_clear_status}");
@ -185,8 +195,8 @@ class InvoiceCommand extends Command
if ($output->isVerbose()) { if ($output->isVerbose()) {
$output->writeln("Csv file downloaded to: <info>{$file}</info>"); $output->writeln("Csv file downloaded to: <info>{$file}</info>");
} }
$data = $this->csv->getInvoiceData($file); $data = $this->csv->getInvoiceData($file);
if (!empty($expenses)) { if (! empty($expenses)) {
$data = array_merge($data, $expenses); $data = array_merge($data, $expenses);
} }
// $table = $this->generateTable($output, $data); // $table = $this->generateTable($output, $data);
@ -219,17 +229,20 @@ class InvoiceCommand extends Command
return Command::SUCCESS; return Command::SUCCESS;
} }
protected function getTable(OutputInterface $output, array $data) :Table { protected function getTable(OutputInterface $output, array $data) : Table
$rows = $this->csv->generateTable($data); {
$rows = $this->csv->generateTable($data);
$table = new Table($output); $table = new Table($output);
$table->setHeaders([ $table->setHeaders([
'Project', 'Hours', 'Rate', 'Price', 'Project',
'Hours',
'Rate',
'Price',
]); ]);
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
if (!$row) { if (! $row) {
$rows[$key] = new TableSeparator(); $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. // Check which elements in array are null.
$rows[$key] = [new TableCell($row[2], ['colspan' => 3]), $row[3]]; $rows[$key] = [new TableCell($row[2], ['colspan' => 3]), $row[3]];
} }
@ -248,7 +261,10 @@ class InvoiceCommand extends Command
{ {
$table = new Table($output); $table = new Table($output);
$table->setHeaders([ $table->setHeaders([
'Project', 'Hours', 'Rate', 'Price', 'Project',
'Hours',
'Rate',
'Price',
]); ]);
[$rows, $totalHours, $totalPrice] = [[], 0, 0]; [$rows, $totalHours, $totalPrice] = [[], 0, 0];
$projectsConfig = $this->configuration->get('projects'); $projectsConfig = $this->configuration->get('projects');
@ -274,9 +290,9 @@ class InvoiceCommand extends Command
$totalPrice += $price; $totalPrice += $price;
unset($data[$name]); unset($data[$name]);
} }
if (!empty($data)) { if (! empty($data)) {
foreach ($data as $name => $value) { 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[] * @return Expenses[]
*/ */
protected function getExpenses($expenses) { protected function getExpenses($expenses)
{
$output = []; $output = [];
if (is_string($expenses)) { if (is_string($expenses)) {
foreach (explode(';', $expenses) as $expense) { foreach (explode(';', $expenses) as $expense) {
[$name, $value] = explode('=', $expense); [$name, $value] = explode('=', $expense);
$output[] = new Expenses($name, (float) $value); $output[] = new Expenses($name, (float) $value);
} }
} } else {
else { $continue = true;
$continue = TRUE;
while ($continue) { 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: '); $value = (float) readline('Enter expenses value: ');
if (!empty($name)) { if (! empty($name)) {
$output[] = new Expenses($name, $value); $output[] = new Expenses($name, $value);
} } else {
else { $continue = false;
$continue = FALSE;
} }
} }
} }
return $output; return $output;
} }
protected function getCustomWorkOrExpenses($custom, $type) { protected function getCustomWorkOrExpenses($custom, $type)
{
$output = []; $output = [];
if (is_string($custom)) { if (is_string($custom)) {
foreach (explode(';', $custom) as $item) { foreach (explode(';', $custom) as $item) {
[$name, $value] = explode('=', $item); [$name, $value] = explode('=', $item);
$output[] = $this->createInvoiceElement($name, (float) $value, $type); $output[] = $this->createInvoiceElement($name, (float) $value, $type);
} }
} else { } else {
$continue = TRUE; $continue = true;
if ($type == self::TYPE_WORK) { if ($type === self::TYPE_WORK) {
$message_name = 'Enter project name or leave empty to stop: '; $message_name = 'Enter project name or leave empty to stop: ';
$message_value = 'Enter time spent of project: '; $message_value = 'Enter time spent of project: ';
} elseif ($type == self::TYPE_EXPENSE) { } elseif ($type === self::TYPE_EXPENSE) {
$message_name = 'Enter expenses name or leave empty to stop: '; $message_name = 'Enter expenses name or leave empty to stop: ';
$message_value = 'Enter expenses value: '; $message_value = 'Enter expenses value: ';
} }
while ($continue) { while ($continue) {
$name = readline($message_name); $name = readline($message_name);
$value = (float) readline($message_value); $value = (float) readline($message_value);
if (!empty($name)) { if (! empty($name)) {
$output[] = $this->createInvoiceElement($name, $value, $type); $output[] = $this->createInvoiceElement($name, $value, $type);
} else { } else {
$continue = FALSE; $continue = false;
} }
} }
} }
return $output; return $output;
} }
protected function createInvoiceElement(string $name, float $value, int $type) { protected function createInvoiceElement(string $name, float $value, int $type)
if ($type == self::TYPE_WORK) { {
if ($type === self::TYPE_WORK) {
return new WorkInvoiceElement($name, (float) $value); return new WorkInvoiceElement($name, (float) $value);
} elseif ($type == self::TYPE_EXPENSE) { } elseif ($type === self::TYPE_EXPENSE) {
return new Expenses($name, (float) $value); return new Expenses($name, (float) $value);
} }
throw new \Exception('Unkown invoice element type.'); throw new Exception('Unkown invoice element type.');
} }
} }

View File

@ -9,29 +9,36 @@ use RprtCli\Utils\CsvReport\ReportCsvInterface;
use RprtCli\Utils\TimeTrackingServices\YoutrackInterface; use RprtCli\Utils\TimeTrackingServices\YoutrackInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableCell;
use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; 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 $trackingService;
protected $config; protected $config;
protected $csv; 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; $this->config = $configuration;
// @TODO generalize tracking service. // @TODO generalize tracking service.
$this->trackingService = $tracking_service; $this->trackingService = $tracking_service;
$this->csv = $csv; $this->csv = $csv;
parent::__construct($name); parent::__construct($name);
} }
protected function configure() :void { protected function configure() : void
{
$this->setName('report'); $this->setName('report');
$this->setDescription('Get a time-tracking report into command line.'); $this->setDescription('Get a time-tracking report into command line.');
$this->addOption( $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')) { if ($timeRange = $input->getOption('time-range')) {
// @TODO: Implement time range option: // @TODO: Implement time range option:
// - Request workTime items from tracking service // - Request workTime items from tracking service
@ -62,29 +70,31 @@ class ReportCommand extends Command {
$reports = $this->trackingService->listReports(); $reports = $this->trackingService->listReports();
// Could just parse a csv file or actually get workItems from youtrack ... // Could just parse a csv file or actually get workItems from youtrack ...
if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) { if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
if ($report = $input->getOption('report')) { if (! $report = $input->getOption('report')) {
$this->trackingService->setReportId($report); // @TODO Make selection nicer.
} else { // QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
$count = 1; $helper = $this->getHelper('question');
foreach ($reports as $id => $name) { $question = new ChoiceQuestion('Select report:', $reports);
$output->writeln("[{$count}] {$name} ({$id})"); $question
$count++; ->setErrorMessage('Report %s does not exist!')
} ->setAutocompleterValues($reports);
$output->writeln("[{$count}] None (null)"); $report = $helper->ask($input, $output, $question);
$report = readline('Select id of the report: '); $output->writeln('Report ' . $report . ' selected.');
// Asume people are literate. }
if (!in_array($report, array_keys($reports) )) { // If parameter option is not recognised check if report name was given.
$output->writeln('Non-existing report. Exiting.'); if (! isset($reports[$report])) {
if (! isset(array_flip($reports)[$report])) {
$output->writeln('Non-existing report ' . $report . '. Exiting.');
return Command::SUCCESS; return Command::SUCCESS;
} }
$this->trackingService->setReportId($report); $report = array_flip($reports)[$report];
} }
} $this->trackingService->setReportId($report);
elseif ($report = $this->config->get('tracking_service.youtrack.report.default')) { } elseif ($report = $this->config->get('tracking_service.youtrack.report.default')) {
$this->trackingService->setReportId($report); $this->trackingService->setReportId($report);
} }
// Currently we only support csv download. // Currently we only support csv download.
$report_id = $this->trackingService->getReportId(); $report_id = $this->trackingService->getReportId();
$report_name = $reports[$report_id]; $report_name = $reports[$report_id];
// Code duplication. // Code duplication.
$cache_clear_status = $this->trackingService->clearReportCache($report_id); $cache_clear_status = $this->trackingService->clearReportCache($report_id);
@ -96,7 +106,7 @@ class ReportCommand extends Command {
$output->writeln("Csv file downloaded to: <info>{$file}</info>"); $output->writeln("Csv file downloaded to: <info>{$file}</info>");
} }
$output->writeln("report: <info>{$report_name}</info>"); $output->writeln("report: <info>{$report_name}</info>");
$data = $this->csv->generateReportTable($file); $data = $this->csv->generateReportTable($file);
$table = $this->buildTable($output, $data); $table = $this->buildTable($output, $data);
$table->render(); $table->render();
@ -108,13 +118,17 @@ class ReportCommand extends Command {
* *
* @TODO: Code duplication with InvoiceCommand::getTable. * @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 = new Table($output);
$table->setHeaders([ $table->setHeaders([
'Ticket Id', 'Name', 'Time', 'Estimation', 'Ticket Id',
'Name',
'Time',
'Estimation',
]); ]);
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
if (!$row) { if (! $row) {
$rows[$key] = new TableSeparator(); $rows[$key] = new TableSeparator();
} elseif (is_array($row) && is_null($row[0]) && is_null($row[2])) { } elseif (is_array($row) && is_null($row[0]) && is_null($row[2])) {
// Check which elements in array are null. // Check which elements in array are null.
@ -124,5 +138,4 @@ class ReportCommand extends Command {
$table->setRows($rows); $table->setRows($rows);
return $table; return $table;
} }
} }

View File

@ -14,8 +14,8 @@ use Symfony\Component\Console\Command\Command;
* Later connect this command to the Emacs and have your time tracked directly * Later connect this command to the Emacs and have your time tracked directly
* from orgmode. * from orgmode.
*/ */
class TrackCommand extends Command { class TrackCommand extends Command
{
protected $config; protected $config;
protected $youtrack; protected $youtrack;
@ -25,19 +25,17 @@ class TrackCommand extends Command {
YoutrackInterface $youtrack, YoutrackInterface $youtrack,
?string $name = null ?string $name = null
) { ) {
$this->config = $config; $this->config = $config;
$this->youtrack = $youtrack; $this->youtrack = $youtrack;
parent::__construct($name); parent::__construct($name);
} }
protected function configure(): void { protected function configure() : void
{
$this->setName('youtrack'); $this->setName('youtrack');
$this->setDescription('Track time into your youtrack service'); $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]'); $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 or arguments? Technically they are arguments but default value could be provided by config.
// Options are more suitable. // Options are more suitable.
} }
} }

View File

@ -4,19 +4,14 @@ declare(strict_types=1);
// src/Utils/Configuration/TranslationService.php // src/Utils/Configuration/TranslationService.php
use Symfony\Component\Translation\Translator;
use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\Configuration\ConfigurationInterface;
class TranslationService { class TranslationService
{
protected $config; protected $config;
public function __construct(ConfigurationInterface $configuration) public function __construct(ConfigurationInterface $configuration)
{ {
$this->config = $configuration; $this->config = $configuration;
} }
} }

View File

@ -10,8 +10,11 @@ use function array_key_first;
use function array_keys; use function array_keys;
use function fgetcsv; use function fgetcsv;
use function fopen; use function fopen;
use function is_array;
use function number_format;
use function preg_match; use function preg_match;
use function reset; use function reset;
use function var_dump;
/** /**
* Creates a report of projects and hours. * 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. * Get correct values from the raw data lines of csv.
* *
* @param array $rawData *
* Columns with data are specified in config. * Columns with data are specified in config.
* *
* @return array
* Project key and unit of time spent. * Project key and unit of time spent.
*/ */
protected function parseCsvFile(array $rawData) : array protected function parseCsvFile(array $rawData) : array
@ -78,10 +80,11 @@ class CsvReport implements CsvReportInterface
return []; return [];
} }
public function arangeDataForDefaultPdfExport(array $data): array { public function arangeDataForDefaultPdfExport(array $data) : array
{
[$rows, $totalHours, $totalPrice] = [[], 0, 0]; [$rows, $totalHours, $totalPrice] = [[], 0, 0];
$projectsConfig = $this->configurationService->get('projects'); $projectsConfig = $this->configurationService->get('projects');
$header = $this->configurationService->get('export.labels', null); $header = $this->configurationService->get('export.labels', null);
if (is_array($header)) { if (is_array($header)) {
$rows[] = $header; $rows[] = $header;
} }

View File

@ -22,9 +22,8 @@ interface CsvReportInterface
/** /**
* Data for default drunomics pdf export. * Data for default drunomics pdf export.
* *
* @param array $data *
* Parsed data from csv report. * Parsed data from csv report.
*/ */
public function arangeDataForDefaultPdfExport(array $data): array; public function arangeDataForDefaultPdfExport(array $data) : array;
} }

View File

@ -11,17 +11,25 @@ use RprtCli\ValueObjects\WorkInvoiceElementInterface;
use function array_key_first; use function array_key_first;
use function array_keys; use function array_keys;
use function array_unshift;
use function array_values;
use function explode;
use function fgetcsv; use function fgetcsv;
use function fopen; use function fopen;
use function implode;
use function is_array;
use function is_numeric;
use function number_format;
use function preg_match; use function preg_match;
use function reset; use function reset;
use function substr;
/** /**
* Creates a report of projects and hours. * Creates a report of projects and hours.
* *
* Uses value objects instead of arrays. * Uses value objects instead of arrays.
*/ */
class ReportCsv implements ReportCsvInterface class ReportCsv implements ReportCsvInterface
{ {
/** /**
* A configuration service. * A configuration service.
@ -67,10 +75,9 @@ class ReportCsv implements ReportCsvInterface
/** /**
* Get correct values from the raw data lines of csv. * Get correct values from the raw data lines of csv.
* *
* @param array $rawData *
* Columns with data are specified in config. * Columns with data are specified in config.
* *
* @return array
* Project key and unit of time spent. * Project key and unit of time spent.
*/ */
protected function parseCsvFile(array $rawData) : array protected function parseCsvFile(array $rawData) : array
@ -87,9 +94,10 @@ class ReportCsv implements ReportCsvInterface
/** /**
* Input is array of Work elements and expenses. * Input is array of Work elements and expenses.
*/ */
public function generateTable(array $data): array { public function generateTable(array $data) : array
[$rows, $totalHours, $totalPrice, $add_separator] = [[], 0, 0, FALSE]; {
$projectsConfig = $this->configurationService->get('projects'); [$rows, $totalHours, $totalPrice, $add_separator] = [[], 0, 0, false];
$projectsConfig = $this->configurationService->get('projects');
// $header = $this->configurationService->get('export.labels', null); // $header = $this->configurationService->get('export.labels', null);
$header = null; $header = null;
if (is_array($header)) { if (is_array($header)) {
@ -98,36 +106,36 @@ class ReportCsv implements ReportCsvInterface
// First only list work invoice elements. // First only list work invoice elements.
foreach ($data as $key => $invoice_element) { foreach ($data as $key => $invoice_element) {
if ($invoice_element instanceof WorkInvoiceElementInterface) { if ($invoice_element instanceof WorkInvoiceElementInterface) {
$add_separator = TRUE; $add_separator = true;
$project = $invoice_element->getName(); $project = $invoice_element->getName();
$time = $invoice_element->getTime(); $time = $invoice_element->getTime();
$config = $projectsConfig[$project]; $config = $projectsConfig[$project];
$hours = $config['time_format'] == 'm' ? $time/60 : $time; $hours = $config['time_format'] === 'm' ? $time / 60 : $time;
$price = $hours * (float) $config['price']; $price = $hours * (float) $config['price'];
$row = [ $row = [
$config['name'] ?? $project, $config['name'] ?? $project,
number_format($hours, 2, ',', '.'), number_format($hours, 2, ',', '.'),
number_format($config['price'], 2, ',', '.'), number_format($config['price'], 2, ',', '.'),
number_format($price, 2, ',', '.'), number_format($price, 2, ',', '.'),
]; ];
$totalHours += $hours; $totalHours += $hours;
$totalPrice += $price; $totalPrice += $price;
$rows[] = $row; $rows[] = $row;
unset($data[$key]); unset($data[$key]);
} }
} }
if ($add_separator) { if ($add_separator) {
// @TODO replace separators with constants for normal separating. // @TODO replace separators with constants for normal separating.
$rows[] = null; $rows[] = null;
$rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), ' ', number_format($totalPrice, 2, ',', '.')]; $rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), ' ', number_format($totalPrice, 2, ',', '.')];
$add_separator = FALSE; $add_separator = false;
} }
if (empty($data)) { if (empty($data)) {
$add_separator = TRUE; $add_separator = true;
} }
foreach ($data as $invoice_element) { foreach ($data as $invoice_element) {
if ($invoice_element instanceof ExpensesInterface) { if ($invoice_element instanceof ExpensesInterface) {
if (!isset($added_expenses)) { if (! isset($added_expenses)) {
// Separator 0: Make next line bold and centered. // Separator 0: Make next line bold and centered.
$rows[] = ReportCsvInterface::SEPARATOR_HARD; $rows[] = ReportCsvInterface::SEPARATOR_HARD;
$rows[] = [ $rows[] = [
@ -137,17 +145,17 @@ class ReportCsv implements ReportCsvInterface
'EUR', 'EUR',
]; ];
// Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml. // Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml.
$rows[] = ReportCsvInterface::SEPARATOR_SOFT; $rows[] = ReportCsvInterface::SEPARATOR_SOFT;
$added_expenses = TRUE; $added_expenses = true;
} }
$add_separator = TRUE; $add_separator = true;
$rows[] = [ $rows[] = [
null, null,
null, null,
$invoice_element->getName(), $invoice_element->getName(),
number_format($invoice_element->getValue(), 2, ',', '.'), number_format($invoice_element->getValue(), 2, ',', '.'),
]; ];
$totalPrice += $invoice_element->getValue(); $totalPrice += $invoice_element->getValue();
} }
} }
if ($add_separator) { if ($add_separator) {
@ -160,19 +168,21 @@ class ReportCsv implements ReportCsvInterface
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function arangeDataForDefaultPdfExport(array $data) :array { public function arangeDataForDefaultPdfExport(array $data) : array
$rows = $this->generateTable($data); {
$rows = $this->generateTable($data);
$header = $this->configurationService->get('export.labels', null); $header = $this->configurationService->get('export.labels', null);
array_unshift($rows, $header); array_unshift($rows, $header);
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
if (!$row) { if (! $row) {
unset($data[$key]); unset($data[$key]);
} }
} }
return $rows; return $rows;
} }
public function generateReportTable(string $filePath) { public function generateReportTable(string $filePath)
{
// ticket-id, ticket-name, time-spent // ticket-id, ticket-name, time-spent
$data = $this->parseReportData($filePath); $data = $this->parseReportData($filePath);
if (empty($data)) { if (empty($data)) {
@ -180,51 +190,51 @@ class ReportCsv implements ReportCsvInterface
} }
[$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], []]; [$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], []];
foreach ($data as $line) { foreach ($data as $line) {
$project = explode('-', $line['id'])[0]; $project = explode('-', $line['id'])[0];
$previous_project = explode('-', $previous)[0]; $previous_project = explode('-', $previous)[0];
if ($project !== $previous_project) { if ($project !== $previous_project) {
// When project changes, add a sum of time for that project. // When project changes, add a sum of time for that project.
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM; $table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
$table[] = [null, $previous_project, null, $project_time/60]; $table[] = [null, $previous_project, null, $project_time / 60];
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM; $table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
$time_sum += (float) $project_time; $time_sum += (float) $project_time;
$project_time = 0; $project_time = 0;
$all_projects[] = $project; $all_projects[] = $project;
} }
$project_time += (float) $line['time']; $project_time += (float) $line['time'];
$previous = $line['id']; $previous = $line['id'];
$table[] = array_values($line); $table[] = array_values($line);
} }
// Add sum for the last project. // Add sum for the last project.
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM; $table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
$table[] = [null, $project, null, $project_time / 60]; $table[] = [null, $project, null, $project_time / 60];
$time_sum += (float) $project_time; $time_sum += (float) $project_time;
// $all_projects[] = $project; // $all_projects[] = $project;
// Add a sum of time for whole day. // Add a sum of time for whole day.
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM; $table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
$table[] = [null, implode(', ', $all_projects), null, $time_sum/60]; $table[] = [null, implode(', ', $all_projects), null, $time_sum / 60];
return $table; return $table;
} }
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
protected function parseReportData(string $filePath): array protected function parseReportData(string $filePath) : array
{ {
$output = []; $output = [];
// @TODO replace with config service. // @TODO replace with config service.
// $config = $this->dummyConfig()['projects']; // $config = $this->dummyConfig()['projects'];
if ($file = fopen($filePath, 'r')) { if ($file = fopen($filePath, 'r')) {
while (($line = fgetcsv($file)) !== false) { while (($line = fgetcsv($file)) !== false) {
if (!is_numeric($line[4])) { if (! is_numeric($line[4])) {
// Skip header at least. // Skip header at least.
continue; continue;
} }
// @TODO validate line // @TODO validate line
$output[] = [ $output[] = [
'id' => $line[1], 'id' => $line[1],
'name' => substr($line[2], 0, 60), 'name' => substr($line[2], 0, 60),
'time' => $line[4], 'time' => $line[4],
'estimation' => $line[3], 'estimation' => $line[3],
]; ];
} }
@ -232,7 +242,6 @@ class ReportCsv implements ReportCsvInterface
return $output; return $output;
} }
/** /**
* Should be moved into test class. * Should be moved into test class.
*/ */

View File

@ -9,16 +9,15 @@ namespace RprtCli\Utils\CsvReport;
*/ */
interface ReportCsvInterface interface ReportCsvInterface
{ {
/** /**
* Normal separator. * Normal separator.
*/ */
const SEPARATOR_SOFT = FALSE; const SEPARATOR_SOFT = false;
/** /**
* Medium separator. Next line bold. * Medium separator. Next line bold.
*/ */
const SEPARATOR_MEDIUM = NULL; const SEPARATOR_MEDIUM = null;
/** /**
* Next line should be bold and centered. * Next line should be bold and centered.
@ -33,20 +32,20 @@ interface ReportCsvInterface
* *
* Project key as key and number of hours as value. * 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. * Returns array of rows created from array of InvoiceElements.
* *
* If row is null, it is meant to be a table separator. * 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. * Data for default drunomics pdf export.
* *
* @param array $data *
* Parsed data from csv report. * Parsed data from csv report.
*/ */
public function arangeDataForDefaultPdfExport(array $data): array; public function arangeDataForDefaultPdfExport(array $data) : array;
} }

View File

@ -7,6 +7,6 @@ namespace RprtCli\Utils\Mailer;
/** /**
* Methods for symfony (swift)mailer service. * Methods for symfony (swift)mailer service.
*/ */
interface MailerInterface { interface MailerInterface
{
} }

View File

@ -6,21 +6,33 @@ declare(strict_types=1);
namespace RprtCli\Utils\Mailer; namespace RprtCli\Utils\Mailer;
use Exception;
use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\Utils\PdfExport\PdfExportInterface; 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\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. * Send emails with invoices as attachments.
* *
* https://symfony.com/doc/current/mailer.html * https://symfony.com/doc/current/mailer.html
*/ */
class MailerService implements MailerInterface { class MailerService implements MailerInterface
{
protected $config; protected $config;
protected $pdf; protected $pdf;
@ -34,31 +46,35 @@ class MailerService implements MailerInterface {
protected $email; protected $email;
public function __construct(ConfigurationInterface $config, PdfExportInterface $pdf) { public function __construct(ConfigurationInterface $config, PdfExportInterface $pdf)
{
$this->config = $config; $this->config = $config;
$this->pdf = $pdf; $this->pdf = $pdf;
} }
public function setRecipients(array $to): void public function setRecipients(array $to) : void
{ {
$this->to = $to; $this->to = $to;
} }
public function setSubject(string $subject): void { public function setSubject(string $subject) : void
{
$this->subject = $subject; $this->subject = $subject;
} }
public function setAttachment(string $path): void { public function setAttachment(string $path) : void
{
// @TODO - add some error handling. // @TODO - add some error handling.
$this->attachment = $path; $this->attachment = $path;
} }
public function getProperty(string $property) { public function getProperty(string $property)
{
// Only for simple value properies - string and numbers. // Only for simple value properies - string and numbers.
// from, to, subject. // from, to, subject.
if (!isset($this->{$property})) { if (! isset($this->{$property})) {
$value = $this->config->get('email.' . $property, FALSE); $value = $this->config->get('email.' . $property, false);
if (!$value) { if (! $value) {
$value = readline("Property {$property} is not configured. Enter value: "); $value = readline("Property {$property} is not configured. Enter value: ");
} }
$this->{$property} = $value; $this->{$property} = $value;
@ -66,10 +82,11 @@ class MailerService implements MailerInterface {
return $this->{$property}; return $this->{$property};
} }
protected function getRecipients() { protected function getRecipients()
if (!isset($this->to)) { {
$value = $this->config->get('email.to', FALSE); if (! isset($this->to)) {
if (!$value) { $value = $this->config->get('email.to', false);
if (! $value) {
$value = explode(',', readline('Provide recipients\' emails separated by a comma: ')); $value = explode(',', readline('Provide recipients\' emails separated by a comma: '));
} }
$this->to = $value; $this->to = $value;
@ -77,7 +94,8 @@ class MailerService implements MailerInterface {
return $this->to; return $this->to;
} }
private function readPassword($prompt = "Enter Password:") { private function readPassword($prompt = "Enter Password:")
{
echo $prompt; echo $prompt;
system('stty -echo'); system('stty -echo');
$password = trim(fgets(STDIN)); $password = trim(fgets(STDIN));
@ -85,10 +103,11 @@ class MailerService implements MailerInterface {
return $password; return $password;
} }
protected function getPasswordProperty() { protected function getPasswordProperty()
if (!isset($this->password)) { {
$value = $this->config->get('email.password', FALSE); if (! isset($this->password)) {
if (!$value) { $value = $this->config->get('email.password', false);
if (! $value) {
$value = $this->readPassword(); $value = $this->readPassword();
} }
$this->password = $value; $this->password = $value;
@ -96,8 +115,8 @@ class MailerService implements MailerInterface {
return $this->password; 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 = new Email();
$email->from($from); $email->from($from);
$email->to(...$to); $email->to(...$to);
@ -105,21 +124,21 @@ class MailerService implements MailerInterface {
// https://github.com/symfony/mailer // https://github.com/symfony/mailer
$email->subject($subject); $email->subject($subject);
$email->text($text); $email->text($text);
if (!empty($attachment)) { if (! empty($attachment)) {
if (!isset($attachment['path'])) { if (! isset($attachment['path'])) {
var_dump('Attachment path missing!'); var_dump('Attachment path missing!');
} } else {
else {
$email->attachFromPath($attachment['path'], $attachment['name'] ?? null, $attachment['type'] ?? null); $email->attachFromPath($attachment['path'], $attachment['name'] ?? null, $attachment['type'] ?? null);
} }
} }
// Not sure whether it would be nicer to use class property instead of variable. // Not sure whether it would be nicer to use class property instead of variable.
$transport = $this->getTransport(); $transport = $this->getTransport();
$mailer = $this->getMailer($transport); $mailer = $this->getMailer($transport);
$mailer->send($email); $mailer->send($email);
} }
public function getTransport() { public function getTransport()
{
// @TODO remove username and password from config. // @TODO remove username and password from config.
$username = rawurlencode($this->getProperty('username')); $username = rawurlencode($this->getProperty('username'));
$password = rawurlencode($this->getPasswordProperty()); $password = rawurlencode($this->getPasswordProperty());
@ -129,13 +148,15 @@ class MailerService implements MailerInterface {
return Transport::fromDsn($mailer_dsn); return Transport::fromDsn($mailer_dsn);
} }
public function getMailer($transport) { public function getMailer($transport)
{
return new Mailer($transport); return new Mailer($transport);
} }
public function sendDefaultMail(string $output): void { public function sendDefaultMail(string $output) : void
$tokens = $this->pdf->gatherTokensForTemplate($this->getEmailTemplatePath(), FALSE, $this->getDefaultTokens(), 'email.tokens'); {
$text = $this->pdf->replaceTokensInTemplate($this->getEmailTemplatePath(), $tokens); $tokens = $this->pdf->gatherTokensForTemplate($this->getEmailTemplatePath(), false, $this->getDefaultTokens(), 'email.tokens');
$text = $this->pdf->replaceTokensInTemplate($this->getEmailTemplatePath(), $tokens);
$this->sendMail( $this->sendMail(
$this->getProperty('from'), $this->getProperty('from'),
$this->getProperty('to'), $this->getProperty('to'),
@ -145,21 +166,23 @@ class MailerService implements MailerInterface {
); );
} }
public function getDefaultTokens(): array { public function getDefaultTokens() : array
$tokens = []; {
$date = strtotime('-1 month'); $tokens = [];
$date = strtotime('-1 month');
$tokens['month'] = date('F', $date); $tokens['month'] = date('F', $date);
$tokens['year'] = date('Y', $date); $tokens['year'] = date('Y', $date);
return $tokens; return $tokens;
} }
protected function getEmailTemplatePath(): ?string { protected function getEmailTemplatePath() : ?string
if (!isset($this->templatePath)) { {
$template_path = $this->config->get('email.template_path', FALSE); if (! isset($this->templatePath)) {
if (!$template_path) { $template_path = $this->config->get('email.template_path', false);
if (! $template_path) {
$template_path = readline('Enter template file 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!'); throw new Exception('Template file not found!');
} }
$this->templatePath = $template_path; $this->templatePath = $template_path;
@ -167,12 +190,12 @@ class MailerService implements MailerInterface {
return $this->templatePath; return $this->templatePath;
} }
public function setEmailTemplatePath(string $path): void { public function setEmailTemplatePath(string $path) : void
{
if (file_exists($path)) { if (file_exists($path)) {
$this->templatePath = $path; $this->templatePath = $path;
return; return;
} }
throw new Exception('Email template file not found!'); throw new Exception('Email template file not found!');
} }
} }

View File

@ -7,36 +7,35 @@ namespace RprtCli\Utils\PdfExport;
/** /**
* Handles exporting parsed csv data to pdf files. * Handles exporting parsed csv data to pdf files.
*/ */
interface PdfExportInterface { interface PdfExportInterface
{
/** /**
* Retrieves path to template file either from command option, configuration * Retrieves path to template file either from command option, configuration
* or from uer input. * 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. * Creates html table from parsed csv data.
* *
* @param array $data *
* First line is header. The rest of them is table body. * 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. * 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. * Creates and export file.
* *
* @param string $html *
* Template file with tokens replaced. * Template file with tokens replaced.
* *
* @return bool
* True if export was successfull. * True if export was successfull.
*/ */
public function pdfExport(string $html) : bool; public function pdfExport(string $html) : bool;
@ -44,39 +43,37 @@ interface PdfExportInterface {
/** /**
* Goes through the whole process of creating a pdf for the invoice. * Goes through the whole process of creating a pdf for the invoice.
* *
* @param array $nice_data *
* Parsed csv report export data. * Parsed csv report export data.
* *
* @return string
* Path of the pdf file. * 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. * Sets output file override via command paramater.
* *
* @param string $output *
* Path of the output pdf file ('/tmp/test.pdf'). * 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 support multiple templates by adding template id in config.
// @TODO implement twig. // @TODO implement twig.
/** /**
* Get tokens to replace in the template. * Get tokens to replace in the template.
* *
* @param string $template_path *
* Path to the template file (long term plan is to support multiple templates). * Path to the template file (long term plan is to support multiple templates).
* @param bool $skip_missing *
*
* Just skip missing tokens. * Just skip missing tokens.
* @param array $runtime_tokens *
* Provide tokens at runtime of the application (not supported yet). @TODO * Provide tokens at runtime of the application (not supported yet). @TODO
* @param string $config *
* Config path to tokens. * Config path to tokens.
* *
* @return array
* Token keys and values 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;
} }

View File

@ -5,12 +5,23 @@ declare(strict_types=1);
namespace RprtCli\Utils\PdfExport; namespace RprtCli\Utils\PdfExport;
use Exception; use Exception;
use RprtCli\Utils\Configuration\ConfigurationInterface;
use Mpdf\Output\Destination; use Mpdf\Output\Destination;
use RprtCli\Utils\Configuration\ConfigurationInterface;
use RprtCli\Utils\CsvReport\ReportCsvInterface; 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 $templatePath;
protected $output; protected $output;
@ -18,18 +29,20 @@ class PdfExportService implements PdfExportInterface {
protected $mpdf; protected $mpdf;
public function __construct(ConfigurationInterface $config, $mpdf) { public function __construct(ConfigurationInterface $config, $mpdf)
{
$this->config = $config; $this->config = $config;
$this->mpdf = $mpdf; $this->mpdf = $mpdf;
} }
public function getTemplatePath(): ?string { public function getTemplatePath() : ?string
if (!isset($this->templatePath)) { {
$template_path = $this->config->get('export.template_path', FALSE); if (! isset($this->templatePath)) {
if (!$template_path) { $template_path = $this->config->get('export.template_path', false);
if (! $template_path) {
$template_path = readline('Enter template file 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!'); throw new Exception('Template file not found!');
} }
$this->templatePath = $template_path; $this->templatePath = $template_path;
@ -37,7 +50,8 @@ class PdfExportService implements PdfExportInterface {
return $this->templatePath; return $this->templatePath;
} }
public function setTemplatePath(string $path): void { public function setTemplatePath(string $path) : void
{
if (file_exists($path)) { if (file_exists($path)) {
$this->templatePath = $path; $this->templatePath = $path;
return; return;
@ -50,40 +64,39 @@ class PdfExportService implements PdfExportInterface {
// @TODO would it make sense to allow more per user extending? // @TODO would it make sense to allow more per user extending?
// @TODO - too much assumptions on data structure. Create a class with // @TODO - too much assumptions on data structure. Create a class with
// precise data structure and use that to pass the data around. // precise data structure and use that to pass the data around.
public function parsedDataToHtmlTable(array $data): ?string { public function parsedDataToHtmlTable(array $data) : ?string
$table = '<table><thead><tr class="tr-header">'; {
$header = array_shift($data); $table = '<table><thead><tr class="tr-header">';
$header = array_shift($data);
$classes = ''; $classes = '';
foreach ($header as $index => $cell) { foreach ($header as $index => $cell) {
$table .= "<th class=\"th-{$index}\">{$cell}</th>"; $table .= "<th class=\"th-{$index}\">{$cell}</th>";
} }
$table .= '</tr></thead><tbody>'; $table .= '</tr></thead><tbody>';
foreach ($data as $row_index => $row) { foreach ($data as $row_index => $row) {
if (!$row) { if (! $row) {
if ($row === ReportCsvInterface::SEPARATOR_MEDIUM) { if ($row === ReportCsvInterface::SEPARATOR_MEDIUM) {
$classes = 'bold'; $classes = 'bold';
} } elseif ($row === ReportCsvInterface::SEPARATOR_HARD) {
elseif ($row === ReportCsvInterface::SEPARATOR_HARD) {
$classes = 'bold center'; $classes = 'bold center';
} }
continue; continue;
} }
list($cells, $colspan) = [[], 0]; [$cells, $colspan] = [[], 0];
foreach ($row as $index => $cell) { foreach ($row as $index => $cell) {
if (!$cell) { if (! $cell) {
$colspan += 1; $colspan += 1;
continue; continue;
} }
if ($colspan) { if ($colspan) {
$colspan += 1; $colspan += 1;
$cells[] = "<td class=\"td-{$index} colspan\" colspan=\"{$colspan}\">{$cell}</td>"; $cells[] = "<td class=\"td-{$index} colspan\" colspan=\"{$colspan}\">{$cell}</td>";
$colspan = 0; $colspan = 0;
} } else {
else {
$cells[] = "<td class=\"td-{$index}\">{$cell}</td>"; $cells[] = "<td class=\"td-{$index}\">{$cell}</td>";
} }
} }
$rows[] = "<tr class=\"tr-{$row_index} {$classes}\">" . implode($cells) . '</tr>'; $rows[] = "<tr class=\"tr-{$row_index} {$classes}\">" . implode($cells) . '</tr>';
$classes = ''; $classes = '';
} }
$table .= implode('', $rows); $table .= implode('', $rows);
@ -92,7 +105,7 @@ class PdfExportService implements PdfExportInterface {
return $table; 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); $template = file_get_contents($template_path);
foreach ($tokens as $key => $value) { foreach ($tokens as $key => $value) {
@ -102,7 +115,8 @@ class PdfExportService implements PdfExportInterface {
} }
// @TODO write a method to gather tokens. // @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]] // @TODO find substrings of type [[key]]
preg_match_all('/\[\[([a-z0-9-_]+)\]\]/', $template, $match); preg_match_all('/\[\[([a-z0-9-_]+)\]\]/', $template, $match);
return $match[1]; return $match[1];
@ -112,41 +126,40 @@ class PdfExportService implements PdfExportInterface {
/** /**
* Get tokens to replace in the template. * Get tokens to replace in the template.
* *
* @param string $template_path *
* Path to the template file (long term plan is to support multiple templates). * Path to the template file (long term plan is to support multiple templates).
* @param bool $skip_missing *
*
* Just skip missing tokens. * Just skip missing tokens.
* @param array $runtime_tokens *
* Provide tokens at runtime of the application (not supported yet). @TODO * Provide tokens at runtime of the application (not supported yet). @TODO
* @param string $config *
* Config path to tokens. * Config path to tokens.
* *
* @return array
* Token keys and values array. * Token keys and values array.
*/ */
public function gatherTokensForTemplate(string $template_path, bool $skip_missing = FALSE, array $runtime_tokens = [], string $config = 'export.tokens'): 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)); [$tokens, $missing] = [[], []];
$config_tokens = $this->config->get($config); $token_keys = $this->getTokensInTemplate(file_get_contents($template_path));
$config_tokens = $this->config->get($config);
foreach ($token_keys as $token_key) { foreach ($token_keys as $token_key) {
if (isset($runtime_tokens[$token_key])) { if (isset($runtime_tokens[$token_key])) {
$tokens[$token_key] = $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: "); $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]; $tokens[$token_key] = $config_tokens[$token_key];
} } else {
else {
$missing[] = $token_key; $missing[] = $token_key;
} }
} }
return $tokens; return $tokens;
} }
public function pdfExport(string $html, $output = null): bool { public function pdfExport(string $html, $output = null) : bool
$this->mpdf->SetProtection(array('print')); {
$this->mpdf->SetProtection(['print']);
// @TODO make configurable. // @TODO make configurable.
$this->mpdf->SetTitle("Invoice"); $this->mpdf->SetTitle("Invoice");
$this->mpdf->SetAuthor("rprt-cli"); $this->mpdf->SetAuthor("rprt-cli");
@ -156,29 +169,32 @@ class PdfExportService implements PdfExportInterface {
return file_exists($this->output); return file_exists($this->output);
} }
protected function getOutput() { protected function getOutput()
{
if (isset($this->output)) { if (isset($this->output)) {
return $this->output; return $this->output;
} }
$output = $this->config->get('export.output', NULL) ?? readline('Enter output file path: '); $output = $this->config->get('export.output', null) ?? readline('Enter output file path: ');
$date = strtotime("-1 month"); $date = strtotime("-1 month");
$output = str_replace('[[month]]', date('F', $date), $output); $output = str_replace('[[month]]', date('F', $date), $output);
$output = str_replace('[[year]]', date('Y', $date), $output); $output = str_replace('[[year]]', date('Y', $date), $output);
$this->output = $output; $this->output = $output;
return $output; return $output;
} }
public function setOutput(string $path): void { public function setOutput(string $path) : void
{
$this->output = $path; $this->output = $path;
} }
public function fromDefaultDataToPdf(array $data, array $tokens = []): string { public function fromDefaultDataToPdf(array $data, array $tokens = []) : string
$template_path = $this->getTemplatePath(); {
$tokens = $this->defaultTokens(); $template_path = $this->getTemplatePath();
$tokens = $this->defaultTokens();
$tokens['table'] = $this->parsedDataToHtmlTable($data); $tokens['table'] = $this->parsedDataToHtmlTable($data);
$tokens = $this->gatherTokensForTemplate($template_path, FALSE, $tokens); $tokens = $this->gatherTokensForTemplate($template_path, false, $tokens);
$html = $this->replaceTokensInTemplate($template_path, $tokens); $html = $this->replaceTokensInTemplate($template_path, $tokens);
$success = $this->pdfExport($html); $success = $this->pdfExport($html);
if ($success) { if ($success) {
return $this->getOutput(); return $this->getOutput();
} }
@ -188,14 +204,14 @@ class PdfExportService implements PdfExportInterface {
/** /**
* Get default tokens. * Get default tokens.
*/ */
protected function defaultTokens(): array { protected function defaultTokens() : array
$tokens = []; {
$tokens['today'] = date('j. m. y'); $tokens = [];
$month_ago = strtotime('1 month ago'); $tokens['today'] = date('j. m. y');
$tokens['number'] = date('m/y', $month_ago); $month_ago = strtotime('1 month ago');
$tokens['number'] = date('m/y', $month_ago);
$tokens['date_start'] = date('1. 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; return $tokens;
} }
} }

View File

@ -19,12 +19,19 @@ interface YoutrackInterface
/** /**
* Downloads report and returns file path. * Downloads report and returns file path.
* *
* @param string $report_id *
* Youtrack internal report id. * Youtrack internal report id.
* *
* @return NULL|string *
* If fetch was unsuccssefull return false, otherwise the file path. * If fetch was unsuccssefull return false, otherwise the file path.
*/ */
public function downloadReport(string $report_id) : ?string; 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;
} }

View File

@ -6,13 +6,28 @@ declare(strict_types=1);
namespace RprtCli\Utils\TimeTrackingServices; namespace RprtCli\Utils\TimeTrackingServices;
use Exception;
use GuzzleHttp\ClientInterface; use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
use RprtCli\Utils\Configuration\ConfigurationInterface; 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 class YoutrackService implements YoutrackInterface
{ {
protected $ytToken; protected $ytToken;
protected $ytBaseUrl; protected $ytBaseUrl;
@ -24,45 +39,47 @@ class YoutrackService implements YoutrackInterface
public function __construct(ConfigurationInterface $config, ClientInterface $http_client) public function __construct(ConfigurationInterface $config, ClientInterface $http_client)
{ {
$this->config = $config; $this->config = $config;
$this->httpClient = $http_client; $this->httpClient = $http_client;
} }
public function testYoutrackapi(): ?string public function testYoutrackapi() : ?string
{ {
// Get base url from config or add input. // Get base url from config or add input.
// Get token or add input. // Get token or add input.
$path = 'youtrack/api/admin/users/me'; $path = 'youtrack/api/admin/users/me';
$yt_url = $this->getYtUrl($path); $yt_url = $this->getYtUrl($path);
$yt_token = $this->getYtToken(); $yt_token = $this->getYtToken();
$query = ['fields' => 'id,email,fullName']; $query = ['fields' => 'id,email,fullName'];
$headers = [ $headers = [
"Authorization" => "Bearer $yt_token", "Authorization" => "Bearer $yt_token",
'Cache-Control' =>'no-cache', 'Cache-Control' => 'no-cache',
]; ];
$me_response = $this->httpClient->request('GET', $yt_url, [ $me_response = $this->httpClient->request('GET', $yt_url, [
'query' => $query, 'query' => $query,
'headers' => $headers 'headers' => $headers,
]); ]);
$test = (string) $me_response->getBody()->getContents(); $test = (string) $me_response->getBody()->getContents();
$me_json = (array) json_decode($test); $me_json = (array) json_decode($test);
if ($me_json && isset($me_json['fullName'])) { if ($me_json && isset($me_json['fullName'])) {
return $me_json['fullName']; return $me_json['fullName'];
} }
return NULL; return null;
} }
protected function requestYoutrackPath(string $path, array $query) { protected function requestYoutrackPath(string $path, array $query)
$yt_url = $this->getYtUrl($path); {
$yt_url = $this->getYtUrl($path);
$headers = $this->getHeaders(); $headers = $this->getHeaders();
return $this->httpClient->request('GET', $yt_url, [ return $this->httpClient->request('GET', $yt_url, [
'query' => $query, 'query' => $query,
'headers' => $headers 'headers' => $headers,
]); ]);
} }
protected function getHeaders() { protected function getHeaders()
{
$yt_token = $this->getYtToken(); $yt_token = $this->getYtToken();
return [ return [
"Authorization" => "Bearer $yt_token", "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. // --report option value should take precedence.
// @TODO error handling. // @TODO error handling.
@ -78,132 +95,135 @@ class YoutrackService implements YoutrackInterface
return $this->report_id; return $this->report_id;
} }
$yt_report_id = $this->config->get('tracking_service.youtrack.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: '); $yt_report_id = readline('Enter the report id: ');
} }
return $yt_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; $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"; $path = "youtrack/api/reports/$report_id/export/csv";
$query = ['$top' => -1]; $query = ['$top' => -1];
$yt_token = $this->getYtToken(); $yt_token = $this->getYtToken();
$headers = [ $headers = [
'Accept' => 'application/json, text/plain, */*', 'Accept' => 'application/json, text/plain, */*',
// 'Accept-Encoding' => 'gzip, deflate, br', // 'Accept-Encoding' => 'gzip, deflate, br',
// 'Connection' => 'keep-alive', // 'Connection' => 'keep-alive',
'Accept-Language' => 'en-US,en;q=0.5', 'Accept-Language' => 'en-US,en;q=0.5',
"Authorization" => "Bearer $yt_token", "Authorization" => "Bearer $yt_token",
]; ];
try { try {
$csv_response = $this->httpClient->request('GET', $this->getYtUrl($path), [ $csv_response = $this->httpClient->request('GET', $this->getYtUrl($path), [
'headers' => $headers, 'headers' => $headers,
'query' => $query, 'query' => $query,
]); ]);
// Write csv response test into temporary file. // Write csv response test into temporary file.
$csv_file = tempnam(sys_get_temp_dir(), "rprt-csv-{$report_id}"); $csv_file = tempnam(sys_get_temp_dir(), "rprt-csv-{$report_id}");
file_put_contents($csv_file, $csv_response->getBody()); file_put_contents($csv_file, $csv_response->getBody());
return $csv_file; return $csv_file;
} } catch (ClientException $e) {
catch (ClientException $e) {
$status = $e->getResponse()->getStatusCode(); $status = $e->getResponse()->getStatusCode();
if ($status == 409) { if ($status === 409) {
sleep(3); sleep(3);
// @TODO Find a way to break of of loop if necessary! // @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."); var_dump("409 response status during download of report {$report_id}. Sleep 3 and try again.");
return $this->downloadReport($report_id); return $this->downloadReport($report_id);
} }
} } catch (Throwable $t) {
catch (\Throwable $t) {
$status = $t->getMessage(); $status = $t->getMessage();
var_dump($status); 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)) { if (isset($this->ytToken)) {
return $this->ytToken; return $this->ytToken;
} }
$yt_token = $this->config->get('tracking_service.youtrack.auth_token', FALSE); $yt_token = $this->config->get('tracking_service.youtrack.auth_token', false);
if (!$yt_token) { if (! $yt_token) {
$yt_token = readline('Enter your youtrack authentication token: '); $yt_token = readline('Enter your youtrack authentication token: ');
} }
return $yt_token; return $yt_token;
} }
public function setYtToken(string $token): void { public function setYtToken(string $token) : void
{
$this->ytToken = $token; $this->ytToken = $token;
} }
protected function getYtUrl(string $path = ''): ?string { protected function getYtUrl(string $path = '') : ?string
{
if (isset($this->ytBaseUrl)) { if (isset($this->ytBaseUrl)) {
$yt_base_url = $this->ytBaseUrl; $yt_base_url = $this->ytBaseUrl;
} } else {
else { $yt_base_url = $this->config->get('tracking_service.youtrack.base_url', false);
$yt_base_url = $this->config->get('tracking_service.youtrack.base_url', FALSE);
} }
if (empty($yt_base_url)) { if (empty($yt_base_url)) {
$yt_base_url = readline('Enter base url for of the youtrack service: '); $yt_base_url = readline('Enter base url for of the youtrack service: ');
} }
if (!empty($path)) { if (! empty($path)) {
$yt_base_url = $yt_base_url . '/' . trim($path, '/'); $yt_base_url .= '/' . trim($path, '/');
} }
return $yt_base_url; return $yt_base_url;
} }
public function setYtUrl(string $base_url) { public function setYtUrl(string $base_url)
{
$this->ytBaseUrl = $base_url; $this->ytBaseUrl = $base_url;
} }
public function listReports() { public function listReports() : array
{
// Now filter results by own = true; // Now filter results by own = true;
$url = '/youtrack/api/reports'; $url = '/youtrack/api/reports';
$query = [ $query = [
'$top' => -1, '$top' => -1,
'fields' => 'id,name,own,owner(login,name)', 'fields' => 'id,name,own,owner(login,name)',
]; ];
$response = $this->requestYoutrackPath($url, $query); $response = $this->requestYoutrackPath($url, $query);
$body = (string) $response->getBody()->getContents(); $body = (string) $response->getBody()->getContents();
$body_json = (array) json_decode($body); $body_json = (array) json_decode($body);
$my_reports = array_filter($body_json, fn($report) => $report->own); $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->id, $my_reports),
array_map(fn ($r) => $r->name, $my_reports) array_map(fn ($r) => $r->name, $my_reports)
); );
return $reports; return $reports;
} }
public function clearReportCache(string $report_id) :int { public function clearReportCache(string $report_id) : int
$path = "/youtrack/api/reports/${report_id}/status"; {
$query = [ $path = "/youtrack/api/reports/${report_id}/status";
'$top' => -1, $query = [
'fields' => 'calculationInProgress,error(id),errorMessage,isOutdated,lastCalculated,progress,wikifiedErrorMessage' '$top' => -1,
'fields' => 'calculationInProgress,error(id),errorMessage,isOutdated,lastCalculated,progress,wikifiedErrorMessage',
]; ];
$post = [ $post = [
'lastCalculated' => floor(microtime(true) * 1000), 'lastCalculated' => floor(microtime(true) * 1000),
'calculationInProgress' => true, 'calculationInProgress' => true,
'wikifiedErrorMessage' => '', 'wikifiedErrorMessage' => '',
'isOutdated' => false, 'isOutdated' => false,
'progress' => -1, 'progress' => -1,
'error' => null, 'error' => null,
'errorMessage' => null, 'errorMessage' => null,
'$type' => 'ReportStatus' '$type' => 'ReportStatus',
]; ];
$yt_url = $this->getYtUrl($path); $yt_url = $this->getYtUrl($path);
$response = $this->httpClient->request('POST', $yt_url, [ $response = $this->httpClient->request('POST', $yt_url, [
'query' => $query, 'query' => $query,
'headers' => $this->getHeaders(), 'headers' => $this->getHeaders(),
'json' => $post, 'json' => $post,
]); ]);
// $body = (string) $response->getBody()->getContents(); // $body = (string) $response->getBody()->getContents();
// var_dump($body); // var_dump($body);
return $response->getStatusCode(); return $response->getStatusCode();
} }
} }

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace RprtCli\ValueObjects; namespace RprtCli\ValueObjects;
class Expenses implements ExpensesInterface { class Expenses implements ExpensesInterface
{
/** /**
* Expenses in current currency. * Expenses in current currency.
*/ */
@ -16,17 +16,18 @@ class Expenses implements ExpensesInterface {
*/ */
private string $name; private string $name;
public function __construct(string $name, float $value) { public function __construct(string $name, float $value)
$this->name = $name; {
$this->name = $name;
$this->value = $value; $this->value = $value;
} }
public function getValue(): float public function getValue() : float
{ {
return $this->value; return $this->value;
} }
public function getName(): string public function getName() : string
{ {
return $this->name; return $this->name;
} }

View File

@ -4,8 +4,7 @@ declare(strict_types=1);
namespace RprtCli\ValueObjects; namespace RprtCli\ValueObjects;
interface ExpensesInterface extends InvoiceElementInterface { interface ExpensesInterface extends InvoiceElementInterface
{
public function getValue() :float; public function getValue() : float;
} }

View File

@ -7,11 +7,10 @@ namespace RprtCli\ValueObjects;
/** /**
* Main interface for invoice elements. * Main interface for invoice elements.
*/ */
interface InvoiceElementInterface { interface InvoiceElementInterface
{
/** /**
* Project or expenses name. * Project or expenses name.
*/ */
public function getName() :string; public function getName() : string;
} }

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace RprtCli\ValueObjects; namespace RprtCli\ValueObjects;
class WorkInvoiceElement implements WorkInvoiceElementInterface { class WorkInvoiceElement implements WorkInvoiceElementInterface
{
private float $time; private float $time;
/** /**
@ -13,17 +13,19 @@ class WorkInvoiceElement implements WorkInvoiceElementInterface {
*/ */
private string $name; private string $name;
public function __construct(string $name, float $time) { public function __construct(string $name, float $time)
{
$this->name = $name; $this->name = $name;
$this->time = $time; $this->time = $time;
} }
public function getTime() :float { public function getTime() : float
{
return $this->time; return $this->time;
} }
public function getName() :string { public function getName() : string
{
return $this->name; return $this->name;
} }
} }

View File

@ -4,12 +4,12 @@ declare(strict_types=1);
namespace RprtCli\ValueObjects; namespace RprtCli\ValueObjects;
interface WorkInvoiceElementInterface extends InvoiceElementInterface { interface WorkInvoiceElementInterface extends InvoiceElementInterface
{
public function getTime() :float ; public function getTime() : float;
/** /**
* Get project name. * Get project name.
*/ */
public function getName() :string ; public function getName() : string;
} }