added annotation based exporter (#1831)
* added user exporter * added project exporter * added customer exporter * added activity exporter
This commit is contained in:
57
src/Export/Annotation/Expose.php
Normal file
57
src/Export/Annotation/Expose.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Annotation;
|
||||
|
||||
use Doctrine\Common\Annotations\Annotation\Enum;
|
||||
use Doctrine\Common\Annotations\Annotation\Required;
|
||||
|
||||
/**
|
||||
* Annotation class for @Expose().
|
||||
*
|
||||
* @Annotation
|
||||
* @Target({"CLASS", "PROPERTY", "METHOD"})
|
||||
*/
|
||||
final class Expose
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
* @Required
|
||||
*/
|
||||
public $label;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
/**
|
||||
* @Enum({"string", "datetime", "date", "time", "integer", "float", "duration", "boolean", "array"})
|
||||
*/
|
||||
public $type = 'string';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $exp = null;
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
if (isset($data['value'])) {
|
||||
$this->name = $data['value'];
|
||||
unset($data['value']);
|
||||
}
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
if (!property_exists(self::class, $key)) {
|
||||
throw new \InvalidArgumentException(sprintf('Unknown property "%s" on annotation "%s".', $key, self::class));
|
||||
}
|
||||
$this->{$key} = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Export/Annotation/Order.php
Normal file
32
src/Export/Annotation/Order.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Annotation;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target({"CLASS"})
|
||||
*/
|
||||
final class Order
|
||||
{
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
public $order = [];
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
if (isset($data['value'])) {
|
||||
$this->order = $data['value'];
|
||||
unset($data['value']);
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/Export/Spreadsheet/AnnotatedObjectExporter.php
Normal file
32
src/Export/Spreadsheet/AnnotatedObjectExporter.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet;
|
||||
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
final class AnnotatedObjectExporter
|
||||
{
|
||||
private $spreadsheetExporter;
|
||||
private $annotationExtractor;
|
||||
|
||||
public function __construct(SpreadsheetExporter $spreadsheetExporter, AnnotationExtractor $annotationExtractor)
|
||||
{
|
||||
$this->spreadsheetExporter = $spreadsheetExporter;
|
||||
$this->annotationExtractor = $annotationExtractor;
|
||||
}
|
||||
|
||||
public function export(string $class, array $entries): Spreadsheet
|
||||
{
|
||||
$columns = $this->annotationExtractor->extract($class);
|
||||
|
||||
return $this->spreadsheetExporter->export($columns, $entries);
|
||||
}
|
||||
}
|
||||
30
src/Export/Spreadsheet/CellFormatter/ArrayFormatter.php
Normal file
30
src/Export/Spreadsheet/CellFormatter/ArrayFormatter.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class ArrayFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_array($value)) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only array is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, implode(';', $value));
|
||||
}
|
||||
}
|
||||
30
src/Export/Spreadsheet/CellFormatter/BooleanFormatter.php
Normal file
30
src/Export/Spreadsheet/CellFormatter/BooleanFormatter.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class BooleanFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!\is_bool($value)) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only boolean is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
interface CellFormatterInterface
|
||||
{
|
||||
/**
|
||||
* @param Worksheet $sheet
|
||||
* @param int $column
|
||||
* @param int $row
|
||||
* @param mixed $value
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void;
|
||||
}
|
||||
33
src/Export/Spreadsheet/CellFormatter/DateFormatter.php
Normal file
33
src/Export/Spreadsheet/CellFormatter/DateFormatter.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class DateFormatter implements CellFormatterInterface
|
||||
{
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$value instanceof \DateTime) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(NumberFormat::FORMAT_DATE_YYYYMMDD2);
|
||||
}
|
||||
}
|
||||
34
src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php
Normal file
34
src/Export/Spreadsheet/CellFormatter/DateTimeFormatter.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class DateTimeFormatter implements CellFormatterInterface
|
||||
{
|
||||
public const DATETIME_FORMAT = 'yyyy-mm-dd hh:mm';
|
||||
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$value instanceof \DateTime) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DATETIME_FORMAT);
|
||||
}
|
||||
}
|
||||
31
src/Export/Spreadsheet/CellFormatter/DurationFormatter.php
Normal file
31
src/Export/Spreadsheet/CellFormatter/DurationFormatter.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class DurationFormatter implements CellFormatterInterface
|
||||
{
|
||||
public const DURATION_FORMAT = '[hh]:mm';
|
||||
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$value = 0;
|
||||
}
|
||||
|
||||
if (!\is_int($value)) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only int is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, sprintf('=%s/86400', $value));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::DURATION_FORMAT);
|
||||
}
|
||||
}
|
||||
34
src/Export/Spreadsheet/CellFormatter/TimeFormatter.php
Normal file
34
src/Export/Spreadsheet/CellFormatter/TimeFormatter.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\CellFormatter;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
class TimeFormatter implements CellFormatterInterface
|
||||
{
|
||||
public const TIME_FORMAT = 'hh:mm';
|
||||
|
||||
public function setFormattedValue(Worksheet $sheet, int $column, int $row, $value): void
|
||||
{
|
||||
if (null === $value) {
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, '');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$value instanceof \DateTime) {
|
||||
throw new \InvalidArgumentException('Unsupported value given, only DateTime is supported');
|
||||
}
|
||||
|
||||
$sheet->setCellValueByColumnAndRow($column, $row, Date::PHPToExcel($value));
|
||||
$sheet->getStyleByColumnAndRow($column, $row)->getNumberFormat()->setFormatCode(self::TIME_FORMAT);
|
||||
}
|
||||
}
|
||||
39
src/Export/Spreadsheet/ColumnDefinition.php
Normal file
39
src/Export/Spreadsheet/ColumnDefinition.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet;
|
||||
|
||||
final class ColumnDefinition
|
||||
{
|
||||
private $label;
|
||||
private $type;
|
||||
private $accessor;
|
||||
|
||||
public function __construct(string $label, string $type, callable $accessor)
|
||||
{
|
||||
$this->label = $label;
|
||||
$this->type = $type;
|
||||
$this->accessor = $accessor;
|
||||
}
|
||||
|
||||
public function getLabel(): string
|
||||
{
|
||||
return $this->label;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function getAccessor(): callable
|
||||
{
|
||||
return $this->accessor;
|
||||
}
|
||||
}
|
||||
36
src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php
Normal file
36
src/Export/Spreadsheet/EntityWithMetaFieldsExporter.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet;
|
||||
|
||||
use App\Event\MetaDisplayEventInterface;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Export\Spreadsheet\Extractor\MetaFieldExtractor;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
final class EntityWithMetaFieldsExporter
|
||||
{
|
||||
private $exporter;
|
||||
private $annotationExtractor;
|
||||
private $metaFieldExtractor;
|
||||
|
||||
public function __construct(SpreadsheetExporter $exporter, AnnotationExtractor $annotationExtractor, MetaFieldExtractor $metaFieldExtractor)
|
||||
{
|
||||
$this->exporter = $exporter;
|
||||
$this->annotationExtractor = $annotationExtractor;
|
||||
$this->metaFieldExtractor = $metaFieldExtractor;
|
||||
}
|
||||
|
||||
public function export(string $class, array $entries, MetaDisplayEventInterface $event): Spreadsheet
|
||||
{
|
||||
$columns = array_merge($this->annotationExtractor->extract($class), $this->metaFieldExtractor->extract($event));
|
||||
|
||||
return $this->exporter->export($columns, $entries);
|
||||
}
|
||||
}
|
||||
150
src/Export/Spreadsheet/Extractor/AnnotationExtractor.php
Normal file
150
src/Export/Spreadsheet/Extractor/AnnotationExtractor.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Export\Annotation\Expose;
|
||||
use App\Export\Annotation\Order;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use Doctrine\Common\Annotations\Reader;
|
||||
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class AnnotationExtractor implements ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @var ExpressionLanguage
|
||||
*/
|
||||
private $expressionLanguage;
|
||||
/**
|
||||
* @var Reader
|
||||
*/
|
||||
private $annotationReader;
|
||||
|
||||
public function __construct(Reader $annotationReader)
|
||||
{
|
||||
$this->annotationReader = $annotationReader;
|
||||
$this->expressionLanguage = new ExpressionLanguage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $value
|
||||
* @return ColumnDefinition[]
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array
|
||||
{
|
||||
if (!\is_string($value)) {
|
||||
throw new ExtractorException('AnnotationExtractor needs a class name (string) for work');
|
||||
}
|
||||
|
||||
try {
|
||||
$reflectionClass = new \ReflectionClass($value);
|
||||
} catch (\ReflectionException $ex) {
|
||||
throw new ExtractorException($ex->getMessage());
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
|
||||
if (null !== ($definitions = $this->annotationReader->getClassAnnotations($reflectionClass))) {
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition instanceof Order) {
|
||||
foreach ($definition->order as $columnName) {
|
||||
$columns[$columnName] = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition instanceof Expose) {
|
||||
if (null === $definition->name) {
|
||||
throw new ExtractorException(sprintf('@Expose needs a name attribute on class level hierarchy, check %s::class', $value));
|
||||
}
|
||||
if (null === $definition->exp) {
|
||||
throw new ExtractorException(sprintf('@Expose needs an expression attribute on class level hierarchy, check %s::class', $value));
|
||||
}
|
||||
|
||||
$parsed = $this->expressionLanguage->parse($definition->exp, ['object']);
|
||||
|
||||
$columns[$definition->name] = new ColumnDefinition(
|
||||
$definition->label,
|
||||
$definition->type,
|
||||
function ($obj) use ($parsed) {
|
||||
return $parsed->getNodes()->evaluate([], ['object' => $obj]);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($reflectionClass->getProperties() as $property) {
|
||||
if (null !== ($definitions = $this->annotationReader->getPropertyAnnotations($property))) {
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition instanceof Expose) {
|
||||
if (null !== $definition->exp) {
|
||||
throw new ExtractorException(sprintf('@Expose only supports the expression attribute on class level hierarchy, check %s::$%s', $value, $property->getName()));
|
||||
}
|
||||
|
||||
$name = empty($definition->name) ? $property->getName() : $definition->name;
|
||||
|
||||
$columns[$name] = new ColumnDefinition(
|
||||
$definition->label,
|
||||
$definition->type,
|
||||
function ($obj) use ($property) {
|
||||
if (!$property->isPublic()) {
|
||||
$property->setAccessible(true);
|
||||
}
|
||||
|
||||
return $property->getValue($obj);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($reflectionClass->getMethods() as $method) {
|
||||
if (null !== ($definitions = $this->annotationReader->getMethodAnnotations($method))) {
|
||||
foreach ($definitions as $definition) {
|
||||
if ($definition instanceof Expose) {
|
||||
if (null !== $definition->exp) {
|
||||
throw new ExtractorException(sprintf('@Expose only supports the expression attribute on class level hierarchy, check %s::%s()', $value, $method->getName()));
|
||||
}
|
||||
$name = empty($definition->name) ? $method->getName() : $definition->name;
|
||||
|
||||
if (\count($method->getParameters()) > 0) {
|
||||
throw new ExtractorException(sprintf('@Expose does not support method %s::%s(...), it has required parameters.', $value, $method->getName()));
|
||||
}
|
||||
|
||||
$columns[$name] = new ColumnDefinition(
|
||||
$definition->label,
|
||||
$definition->type,
|
||||
function ($obj) use ($method) {
|
||||
if (!$method->isPublic()) {
|
||||
$method->setAccessible(true);
|
||||
}
|
||||
|
||||
return $method->invoke($obj);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($columns as $name => $definition) {
|
||||
if (null === $definition) {
|
||||
unset($columns[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_values($columns);
|
||||
}
|
||||
}
|
||||
14
src/Export/Spreadsheet/Extractor/ExtractorException.php
Normal file
14
src/Export/Spreadsheet/Extractor/ExtractorException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\Extractor;
|
||||
|
||||
class ExtractorException extends \Exception
|
||||
{
|
||||
}
|
||||
25
src/Export/Spreadsheet/Extractor/ExtractorInterface.php
Normal file
25
src/Export/Spreadsheet/Extractor/ExtractorInterface.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
|
||||
/**
|
||||
* Extract ColumnDefinition objects from various sources.
|
||||
*/
|
||||
interface ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return ColumnDefinition[]
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array;
|
||||
}
|
||||
68
src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php
Normal file
68
src/Export/Spreadsheet/Extractor/MetaFieldExtractor.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Entity\EntityWithMetaFields;
|
||||
use App\Event\MetaDisplayEventInterface;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class MetaFieldExtractor implements ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param MetaDisplayEventInterface $value
|
||||
* @return ColumnDefinition[]
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array
|
||||
{
|
||||
if (!($value instanceof MetaDisplayEventInterface)) {
|
||||
throw new ExtractorException('MetaFieldExtractor needs a MetaDisplayEventInterface instance for work');
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
|
||||
$this->eventDispatcher->dispatch($value);
|
||||
|
||||
foreach ($value->getFields() as $field) {
|
||||
if (!$field->isVisible()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columns[] = new ColumnDefinition(
|
||||
$field->getLabel(),
|
||||
'string',
|
||||
function (EntityWithMetaFields $entityWithMetaFields) use ($field) {
|
||||
$meta = $entityWithMetaFields->getMetaField($field->getName());
|
||||
if (null === $meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $meta->getValue();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
}
|
||||
68
src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php
Normal file
68
src/Export/Spreadsheet/Extractor/UserPreferenceExtractor.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\Extractor;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\ColumnDefinition;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class UserPreferenceExtractor implements ExtractorInterface
|
||||
{
|
||||
/**
|
||||
* @var EventDispatcherInterface
|
||||
*/
|
||||
private $eventDispatcher;
|
||||
|
||||
public function __construct(EventDispatcherInterface $eventDispatcher)
|
||||
{
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param UserPreferenceDisplayEvent $value
|
||||
* @return ColumnDefinition[]
|
||||
* @throws ExtractorException
|
||||
*/
|
||||
public function extract($value): array
|
||||
{
|
||||
if (!($value instanceof UserPreferenceDisplayEvent)) {
|
||||
throw new ExtractorException('UserPreferenceExtractor needs a UserPreferenceDisplayEvent instance for work');
|
||||
}
|
||||
|
||||
$columns = [];
|
||||
|
||||
$this->eventDispatcher->dispatch($value);
|
||||
|
||||
foreach ($value->getPreferences() as $field) {
|
||||
if (!$field->isEnabled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$columns[] = new ColumnDefinition(
|
||||
$field->getLabel(),
|
||||
'string',
|
||||
function (User $user) use ($field) {
|
||||
$meta = $user->getPreference($field->getName());
|
||||
if (null === $meta) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $meta->getValue();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
}
|
||||
100
src/Export/Spreadsheet/SpreadsheetExporter.php
Normal file
100
src/Export/Spreadsheet/SpreadsheetExporter.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet;
|
||||
|
||||
use App\Export\Spreadsheet\CellFormatter\ArrayFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\BooleanFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\CellFormatterInterface;
|
||||
use App\Export\Spreadsheet\CellFormatter\DateFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\DateTimeFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\DurationFormatter;
|
||||
use App\Export\Spreadsheet\CellFormatter\TimeFormatter;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class SpreadsheetExporter
|
||||
{
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var CellFormatterInterface[]
|
||||
*/
|
||||
private $formatter = [];
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
|
||||
$this->registerCellFormatter('datetime', new DateTimeFormatter());
|
||||
$this->registerCellFormatter('date', new DateFormatter());
|
||||
$this->registerCellFormatter('time', new TimeFormatter());
|
||||
$this->registerCellFormatter('duration', new DurationFormatter());
|
||||
$this->registerCellFormatter('boolean', new BooleanFormatter());
|
||||
$this->registerCellFormatter('array', new ArrayFormatter());
|
||||
}
|
||||
|
||||
public function registerCellFormatter(string $type, CellFormatterInterface $formatter)
|
||||
{
|
||||
$this->formatter[$type] = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ColumnDefinition[] $columns
|
||||
* @param array $entries
|
||||
* @return Spreadsheet
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
*/
|
||||
public function export(array $columns, array $entries): Spreadsheet
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
// Set default row height to automatic, so we can specify wrap text columns later on
|
||||
// without bloating the output file as we would need to store stylesheet info for every cell.
|
||||
// LibreOffice is still not considering this flag, @see https://github.com/PHPOffice/PHPExcel/issues/588
|
||||
// with no solution implemented so nothing we can do about it there.
|
||||
$sheet->getDefaultRowDimension()->setRowHeight(-1);
|
||||
|
||||
$recordsHeaderColumn = 1;
|
||||
$recordsHeaderRow = 1;
|
||||
|
||||
foreach ($columns as $settings) {
|
||||
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans($settings->getLabel()));
|
||||
}
|
||||
|
||||
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||
|
||||
foreach ($entries as $entry) {
|
||||
$entryHeaderColumn = 1;
|
||||
|
||||
foreach ($columns as $settings) {
|
||||
$value = \call_user_func($settings->getAccessor(), $entry);
|
||||
|
||||
if (!\array_key_exists($settings->getType(), $this->formatter)) {
|
||||
$sheet->setCellValueByColumnAndRow($entryHeaderColumn, $entryHeaderRow, $value);
|
||||
} else {
|
||||
$formatter = $this->formatter[$settings->getType()];
|
||||
$formatter->setFormattedValue($sheet, $entryHeaderColumn, $entryHeaderRow, $value);
|
||||
}
|
||||
|
||||
$entryHeaderColumn++;
|
||||
}
|
||||
|
||||
$entryHeaderRow++;
|
||||
}
|
||||
|
||||
return $spreadsheet;
|
||||
}
|
||||
}
|
||||
47
src/Export/Spreadsheet/UserExporter.php
Normal file
47
src/Export/Spreadsheet/UserExporter.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\Extractor\AnnotationExtractor;
|
||||
use App\Export\Spreadsheet\Extractor\UserPreferenceExtractor;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
final class UserExporter
|
||||
{
|
||||
private $exporter;
|
||||
private $annotationExtractor;
|
||||
private $userPreferenceExtractor;
|
||||
|
||||
public function __construct(SpreadsheetExporter $exporter, AnnotationExtractor $annotationExtractor, UserPreferenceExtractor $userPreferenceExtractor)
|
||||
{
|
||||
$this->exporter = $exporter;
|
||||
$this->annotationExtractor = $annotationExtractor;
|
||||
$this->userPreferenceExtractor = $userPreferenceExtractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User[] $entries
|
||||
* @param UserPreferenceDisplayEvent $event
|
||||
* @return Spreadsheet
|
||||
* @throws Extractor\ExtractorException
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
*/
|
||||
public function export(array $entries, UserPreferenceDisplayEvent $event): Spreadsheet
|
||||
{
|
||||
$columns = array_merge(
|
||||
$this->annotationExtractor->extract(User::class),
|
||||
$this->userPreferenceExtractor->extract($event)
|
||||
);
|
||||
|
||||
return $this->exporter->export($columns, $entries);
|
||||
}
|
||||
}
|
||||
71
src/Export/Spreadsheet/Writer/BinaryFileResponseWriter.php
Normal file
71
src/Export/Spreadsheet/Writer/BinaryFileResponseWriter.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\Writer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
|
||||
class BinaryFileResponseWriter implements WriterInterface
|
||||
{
|
||||
/**
|
||||
* @var WriterInterface
|
||||
*/
|
||||
private $writer;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $prefix;
|
||||
|
||||
/**
|
||||
* @param WriterInterface $writer
|
||||
* @param string $prefix is only urlencoded but not validated and can break the response if you pass in invalid character
|
||||
*/
|
||||
public function __construct(WriterInterface $writer, string $prefix)
|
||||
{
|
||||
$this->writer = $writer;
|
||||
$this->prefix = urlencode($prefix);
|
||||
}
|
||||
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return $this->writer->getFileExtension();
|
||||
}
|
||||
|
||||
public function getContentType(): string
|
||||
{
|
||||
return $this->writer->getContentType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(Spreadsheet $spreadsheet, array $options = []): \SplFileInfo
|
||||
{
|
||||
return $this->writer->save($spreadsheet, $options);
|
||||
}
|
||||
|
||||
public function getFileResponse(Spreadsheet $spreadsheet, array $options = []): BinaryFileResponse
|
||||
{
|
||||
$file = $this->save($spreadsheet, $options);
|
||||
|
||||
$filename = $this->prefix . '_' . (new \DateTime())->format('YmdHim') . '.' . $this->writer->getFileExtension();
|
||||
|
||||
$response = new BinaryFileResponse($file);
|
||||
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||
|
||||
$response->headers->set('Content-Type', $this->getContentType());
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
$response->deleteFileAfterSend(true);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
28
src/Export/Spreadsheet/Writer/WriterInterface.php
Normal file
28
src/Export/Spreadsheet/Writer/WriterInterface.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\Writer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
|
||||
interface WriterInterface
|
||||
{
|
||||
public function getFileExtension(): string;
|
||||
|
||||
public function getContentType(): string;
|
||||
|
||||
/**
|
||||
* Save the given spreadsheet
|
||||
*
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @param array $options
|
||||
* @return \SplFileInfo
|
||||
*/
|
||||
public function save(Spreadsheet $spreadsheet, array $options = []): \SplFileInfo;
|
||||
}
|
||||
90
src/Export/Spreadsheet/Writer/XlsxWriter.php
Normal file
90
src/Export/Spreadsheet/Writer/XlsxWriter.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Export\Spreadsheet\Writer;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||||
|
||||
class XlsxWriter implements WriterInterface
|
||||
{
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return 'xlsx';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContentType(): string
|
||||
{
|
||||
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
|
||||
/**
|
||||
* Options:
|
||||
* - freeze (string, default: null) Coordinate of a column to freeze, like: D2
|
||||
* - autofilter (bool, default true) Enable auto filter for header row
|
||||
*
|
||||
* @param Spreadsheet $spreadsheet
|
||||
* @param array $options
|
||||
* @return \SplFileInfo
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function save(Spreadsheet $spreadsheet, array $options = []): \SplFileInfo
|
||||
{
|
||||
$options = array_merge(['autofilter' => true, 'freeze' => null], $options);
|
||||
|
||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-xlsx');
|
||||
if (false === $filename) {
|
||||
throw new \Exception('Could not open temporary file');
|
||||
}
|
||||
|
||||
// Store expensive calculations for later
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
$highestColumn = $sheet->getHighestColumn();
|
||||
|
||||
// Enable auto filter for header row
|
||||
if (false !== $options['autofilter']) {
|
||||
$sheet->setAutoFilter('A1:' . $highestColumn . '1');
|
||||
}
|
||||
|
||||
// Freeze first row and date & time columns for easier navigation
|
||||
if (!empty($options['freeze'])) {
|
||||
$sheet->freezePane($options['freeze']);
|
||||
}
|
||||
|
||||
/** @var string $column */
|
||||
foreach (range('A', $highestColumn) as $column) {
|
||||
// We default to a reasonable auto-width decided by the client,
|
||||
// sadly ->getDefaultColumnDimension() is not supported so it needs
|
||||
// to be specific about what column should be auto sized.
|
||||
$col = $sheet->getColumnDimension($column);
|
||||
|
||||
// If no other width is specified (which defaults to -1)
|
||||
if ((int) $col->getWidth() === -1) {
|
||||
$col->setAutoSize(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Text inside cells should be top left
|
||||
$sheet
|
||||
->getStyle('A2:' . $highestColumn . $highestRow)
|
||||
->getAlignment()
|
||||
->setVertical(Alignment::VERTICAL_TOP)
|
||||
->setHorizontal(Alignment::HORIZONTAL_LEFT);
|
||||
|
||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||
$writer->save($filename);
|
||||
|
||||
return new \SplFileInfo($filename);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user