Showing preview only (417K chars total). Download the full file or copy to clipboard to get everything.
Repository: AnourValar/office
Branch: master
Commit: 899ec9cc1ecb
Files: 37
Total size: 401.4 KB
Directory structure:
gitextract_73d96ymn/
├── .gitattributes
├── .gitignore
├── .php-cs-fixer.php
├── LICENSE
├── README.md
├── composer.json
├── phpcs.xml
├── phpstan.neon
├── phpunit.xml
├── psalm.xml
├── src/
│ ├── Buffer.php
│ ├── DocumentService.php
│ ├── Drivers/
│ │ ├── DocumentInterface.php
│ │ ├── GridInterface.php
│ │ ├── LoadInterface.php
│ │ ├── MixInterface.php
│ │ ├── MultiSheetInterface.php
│ │ ├── PhpSpreadsheetDriver.php
│ │ ├── SaveInterface.php
│ │ ├── SheetsInterface.php
│ │ └── ZipDriver.php
│ ├── Facades/
│ │ ├── ExportGridInterface.php
│ │ ├── ExportGridQueryInterface.php
│ │ └── ExportService.php
│ ├── Format.php
│ ├── Generated.php
│ ├── GridService.php
│ ├── Mixer.php
│ ├── Sheets/
│ │ ├── Parser.php
│ │ └── SchemaMapper.php
│ ├── SheetsService.php
│ ├── Traits/
│ │ ├── Parser.php
│ │ └── XFormat.php
│ └── resources/
│ └── grid.xlsx
└── tests/
├── GridServiceTest.php
├── SheetsParserTest.php
└── TraitsTest.php
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/tests export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.php-cs-fixer.php export-ignore
phpcs.xml export-ignore
phpstan.neon export-ignore
phpunit.xml export-ignore
psalm.xml export-ignore
================================================
FILE: .gitignore
================================================
.phpunit.cache/
vendor/
composer.lock
.php-cs-fixer.cache
================================================
FILE: .php-cs-fixer.php
================================================
<?php
$finder = PhpCsFixer\Finder::create()
->exclude('vendor')
->in(__DIR__);
$config = new PhpCsFixer\Config();
return $config->setRules([
'@PSR12' => true,
])
->setFinder($finder);
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 AnourValar
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Office: Documents | Reports | Grids
## Installation
### Minimal
```bash
composer require anourvalar/office
```
### Phpspreadsheet is required to work with Excel (xlsx).
```bash
composer require phpoffice/phpspreadsheet "^3.10"
```
### Zipstream-php is required to work with Word (docx).
```bash
composer require maennchen/zipstream-php "^3.2"
```
### Mpdf is required to work with PDF.
```bash
composer require mpdf/mpdf: "^8.1"
```
## Generate a document from an XLSX (Excel) template
### One-dimensional table (basic usage)
**template1.xlsx:**

```php
$data = [
// scalar
'vat' => 'No',
'total' => [
'price' => 2004.14,
'qty' => 3,
],
// one-dimensional table
'products' => [
[
'name' => 'Product #1',
'price' => 989,
'qty' => 1,
'date' => new \DateTime('2022-03-30'),
],
[
'name' => 'Product #2',
'price' => 1015.14,
'qty' => 2,
'date' => new \DateTime('2022-03-31'),
],
],
];
// Save to the file
(new \AnourValar\Office\SheetsService())
->generate(
'template1.xlsx', // template filename
$data // markers
)
->saveAs(
'generated_document.xlsx', // filename
\AnourValar\Office\Format::Xlsx // save format
);
// Output to the browser
header('Content-type: ' . \AnourValar\Office\Format::Xlsx->contentType());
header('Content-Disposition: attachment; filename="generated_document.xlsx"');
echo (new \AnourValar\Office\SheetsService())
->generate('template1.xlsx', $data)
->save(\AnourValar\Office\Format::Xlsx);
// Available formats:
// \AnourValar\Office\Format::Xlsx
// \AnourValar\Office\Format::Pdf
// \AnourValar\Office\Format::Html
// \AnourValar\Office\Format::Ods
```
**generated_document.xlsx:**

**The same template with empty data**

### Two-dimensional table
**template2.xlsx:**

```php
$data = [
'best_manager' => 'Sveta',
// two-dimensional table
'managers' => [
'titles' => [[ 'William', 'James', 'Sveta' ]],
'values' => [
[ // additional row
'month' => 'January',
'amount' => [700, 800, 900], // additional columns
],
[
'month' => 'February',
'amount' => [7000, 8000, 9000],
],
[
'month' => 'March',
'amount' => [70000, 80000, 90000],
],
],
],
];
// Save as XLSX (Excel)
(new \AnourValar\Office\SheetsService())
->generate('template2.xlsx', $data)
->saveAs('generated_document.xlsx'); // second argument (format) is optional
```
**generated_document.xlsx:**

### Additional Features
**template3.xlsx:**

```php
$data = [
'foo' => 'Hello',
'bar' => function (SheetsInterface $driver, $column, $row) {
$driver->insertImage('logo.png', $cell, ['width' => 100, 'offset_y' => -45]);
return 'Logo!'; // replace marker "[bar]" with "Logo!"
}
];
(new \AnourValar\Office\SheetsService())
->hookValue(function (SheetsInterface $driver, $column, $row, $value, $sheetIndex) {
// Hook will be called for every cell which is changing
$value .= ' world';
return $value;
})
->generate(
'template3.ods', // ods template
$data,
true // cells auto format instead of template setup
)
->saveAs('generated_document.xlsx');
// Available hooks:
// hookLoad: Closure(SheetsInterface $driver, string $templateFile, Format $templateFormat)
// hookBefore: Closure(SheetsInterface $driver, array &$data)
// hookValue: Closure(SheetsInterface $driver, string $column, int $row, $value, int $sheetIndex)
// hookAfter: Closure(SheetsInterface $driver)
```
**generated_document.xlsx:**

### Dynamic templates
```php
$data = [
'group1' => [
'name' => 'Group 1',
'products' => [
['name' => 'Product 1', 'stock' => 101],
['name' => 'Product 2', 'stock' => 102],
],
],
'group2' => [
'name' => 'Group 2',
'products' => [
['name' => 'Product 3', 'stock' => 103],
['name' => 'Product 4', 'stock' => 104],
],
],
];
(new \AnourValar\Office\SheetsService())
->hookLoad(function ($driver, string $templateFile, $templateFormat) {
// create empty document instead of using existing
return $driver->create();
})
->hookBefore(function ($driver, array &$data) {
// place markers on-fly
$row = 1;
foreach (array_keys($data) as $group) {
// group's title
$driver
->setValue("A$row", "[{$group}.name]")
->mergeCells("A$row:B$row")
->setStyle("A$row", ['align' => 'center', 'bold' => true]);
$row++;
// group's products
$driver
->setValue("A$row", "[$group.products.name]")
->setValue("B$row", "[$group.products.stock]");
$row++;
}
})
->generate('', $data)
->saveAs('generated_document.xlsx');
```
**Dynamic template overview**

**generated_document.xlsx:**

### Merge (union) few documents to a single file
```php
$dataA = ['foo' => 'hello'];
$dataB = ['foo' => 'world'];
$documentA = (new \AnourValar\Office\SheetsService())->generate('template.xlsx', $dataA);
$documentB = (new \AnourValar\Office\SheetsService())->generate('template.xlsx', $dataB);
$mixer = new \AnourValar\Office\Mixer();
$mixer($documentA, $documentB)->saveAs('generated_document.xlsx');
```
### Access the PhpSpreadsheet directly (default driver)
```php
(new \AnourValar\Office\SheetsService())
->hookBefore(function (\AnourValar\Office\Drivers\PhpSpreadsheetDriver $driver, array &$data) {
$spreadsheet = $driver->spreadsheet;
// @see \PhpOffice\PhpSpreadsheet\Spreadsheet
$spreadsheet->createSheet()->setTitle('Foo Bar'); // adding a new Worksheet
})
->generate('template.xlsx', [])
->saveAs('generated_document.xlsx');
```
## Generate a document from an DOCX (Word) template
```php
(new \AnourValar\Office\DocumentService)
->generate('template.docx', ['foo' => 'bar'])
->saveAs('generated_document.docx');
```
**template.docx:**

**generated_document.docx:**

## Export table (Grid)
### Simple usage
```php
$data = [
['William', 3000],
['James', 4000],
['Sveta', 5000],
];
// Save as XLSX (Excel)
(new \AnourValar\Office\GridService())
->generate(
['Name', 'Sales'], // headers
$data // data
)
->saveAs('generated_grid.xlsx');
```
**generated_grid.xlsx:**

### Advanced usage (generators)
```php
$headers = [
['title' => 'Name', 'width' => 30],
['title' => 'Sales'],
];
$data = function () {
yield ['name' => 'William', 'sales' => 3000];
yield ['name' => 'James', 'sales' => 4000];
yield ['name' => 'Sveta', 'sales' => 5000];
};
// Save as XLSX (Excel)
(new \AnourValar\Office\GridService())
->hookHeader(function (GridInterface $driver, mixed $header, $key, $column) {
if (isset($header['width'])) {
$driver->setWidth($column, $header['width']); // column with fixed width
} else {
$driver->autoWidth($column); // column with auto width
}
return $header['title'];
})
->hookRow(function (GridInterface $driver, mixed $row, $key) {
return [
$row['name'],
$row['sales'],
];
})
->hookAfter(function (
GridInterface $driver,
string $headersRange,
string $dataRange,
string $totalRange,
array $columns
) {
$driver->setSheetTitle('Foo');
$driver->setStyle(
$headersRange, // A1:B1
['bold' => true, 'background_color' => 'EEEEEE']
);
$driver->setStyle(
$totalRange, // A1:B4
['borders' => true, 'align' => 'left']
);
})
->generate($headers, $data)
->saveAs('generated_grid.xlsx');
```
**generated_grid.xlsx:**

### Performance
By default, GridService uses PhpSpreadsheetDriver which gives a lot of features and flexability.
The only cons are performance and memory consumtion.
ZipDriver as an alternative is simpler, but much more faster:
```bash
composer require maennchen/zipstream-php "^3.2"
```
```php
$data = [
['William', 3000],
['James', 4000],
['Sveta', 5000],
];
// Save as XLSX (Excel)
(new \AnourValar\Office\GridService(new \AnourValar\Office\Drivers\ZipDriver()))
->generate(['Name', 'Sales'], $data)
->saveAs('generated_grid.xlsx');
```
================================================
FILE: composer.json
================================================
{
"name": "anourvalar/office",
"description": "Generate documents from existing Excel & Word templates | Export tables to Excel (Grids)",
"keywords": [
"excel", "xls", "xlsx", "template", "view", "document", "contract", "report", "generate", "engine", "fill",
"markers", "replacers", "placeholder", "templater", "pdf", "ods", "grid", "export", "generator", "anourvalar",
"variables", "word", "doc", "docx", "table"
],
"homepage": "https://github.com/AnourValar/office",
"license": "MIT",
"require": {
"php": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"phpstan/phpstan": "^2.0",
"friendsofphp/php-cs-fixer": "^3.26",
"squizlabs/php_codesniffer": "^3.7",
"vimeo/psalm": "^7.0"
},
"autoload": {
"psr-4": {"AnourValar\\Office\\": "src/"}
},
"autoload-dev": {
"psr-4": {"AnourValar\\Office\\Tests\\": "tests/"}
}
}
================================================
FILE: phpcs.xml
================================================
<?xml version="1.0"?>
<!-- @see https://pear.php.net/manual/en/package.php.php-codesniffer.annotated-ruleset.php -->
<ruleset name="PHPCS Rules">
<description>PHPCS ruleset</description>
<file>src</file>
<file>tests</file>
<exclude-pattern>src/resources</exclude-pattern>
<!-- Show progress of the run -->
<arg value= "p"/>
<!-- Show sniff codes in all reports -->
<arg value= "s"/>
<!-- Our base rule: set to PSR12 -->
<rule ref="PSR12">
<exclude name="PSR12.Operators.OperatorSpacing.NoSpaceBefore"/>
<exclude name="PSR12.Operators.OperatorSpacing.NoSpaceAfter"/>
<exclude name="PSR12.Traits.UseDeclaration.MultipleImport"/>
<exclude name="Generic.Files.LineLength.TooLong"/>
<exclude name="PSR2.Methods.FunctionClosingBrace.SpacingBeforeClose"/>
<exclude name="PSR12.Classes.OpeningBraceSpace.Found"/>
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingAfterOpen"/>
<exclude name="Squiz.WhiteSpace.ControlStructureSpacing.SpacingBeforeClose"/>
</rule>
<rule ref="PSR1.Methods.CamelCapsMethodName.NotCamelCaps">
<exclude-pattern>tests/</exclude-pattern>
</rule>
<rule ref="Internal.NoCodeFound">
<exclude-pattern>tests/</exclude-pattern>
</rule>
</ruleset>
================================================
FILE: phpstan.neon
================================================
parameters:
paths:
- src
- tests
# The level 10 is the highest level
level: 5
ignoreErrors:
- '#has an uninitialized readonly property#'
- '#Binary operation \"\-\" between non\-empty\-string#'
- '#Call to an undefined method AnourValar\\Office\\Drivers\\SaveInterface\:\:getSheetCount\(\)#'
- '#Call to an undefined method AnourValar\\Office\\Drivers\\SaveInterface\:\:replace\(\)#'
- '#unknown class PhpOffice#'
- '#Class PhpOffice\\PhpSpreadsheet\\Writer\\Csv not found#'
- '#Unsafe usage of new static#'
- '#Call to an undefined method AnourValar\\Office\\Drivers\\SaveInterface\:\:setGrid\(\)#'
- '#Parameter \#1 \$driver of method AnourValar\\Office\\GridService\:\:getGenerator\(\) expects AnourValar\\Office\\Drivers\\GridInterface#'
- '#Class AnourValar\\Office\\Tests\\SheetsParserTest has an uninitialized property \$service#'
- '#has an uninitialized property \$fileSystem#'
- '#\(\) on iterable\.#'
- '#Instantiated class ZipStream#'
- '#unknown class ZipStream#'
- '#has an uninitialized property \$sourceActiveSheetIndex#'
- '#\$format is assigned outside of the constructor#'
- '#has invalid return type PhpOffice#'
- '#\$spreadsheet is assigned outside of the constructor#'
- '#Binary operation \"\+\" between non\-empty\-string#'
- '#Instantiated class PhpOffice#'
- '#expects string, int given#'
- '#has invalid type PhpOffice#'
- '#Match expression does not handle remaining value: mixed#'
- '#Access to an undefined property AnourValar\\Office\\Drivers\\MixInterface\:\:\$spreadsheet#'
- '#Call to an undefined method AnourValar\\Office\\Drivers\\MixInterface\:\:sheet\(\)#'
- '#Call to an undefined method#'
- '#Offset numeric\-string on list\<string> in isset\(\) does not exist\.#'
- '#SaveInterface given\.#'
- '#has invalid return type Illuminate#'
excludePaths:
checkFunctionNameCase: true
checkInternalClassCaseSensitivity: true
reportMaybesInMethodSignatures: true
reportStaticMethodSignatures: true
checkUninitializedProperties: true
checkDynamicProperties: true
reportAlwaysTrueInLastCondition: true
reportWrongPhpDocTypeInVarTag: true
checkMissingCallableSignature: true
reportPossiblyNonexistentGeneralArrayOffset: true
reportPossiblyNonexistentConstantArrayOffset: true
reportAnyTypeWideningInVarTag: true
================================================
FILE: phpunit.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
backupGlobals="false"
colors="true"
stopOnFailure="true"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.3/phpunit.xsd"
cacheDirectory=".phpunit.cache"
backupStaticProperties="false"
>
<coverage/>
<testsuites>
<testsuite name="Office">
<directory>tests</directory>
</testsuite>
</testsuites>
<php></php>
<source>
<include>
<directory suffix=".php">src/</directory>
</include>
</source>
</phpunit>
================================================
FILE: psalm.xml
================================================
<?xml version="1.0"?>
<psalm
errorLevel="7"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
findUnusedCode="true"
findUnusedPsalmSuppress="true"
findUnusedVariablesAndParams="true"
>
<projectFiles>
<directory name="src"/>
<directory name="tests"/>
<ignoreFiles>
<directory name="vendor"/>
</ignoreFiles>
</projectFiles>
<issueHandlers>
<ForbiddenCode errorLevel="error" />
<UnusedClosureParam errorLevel="suppress" />
<PossiblyUnusedMethod errorLevel="suppress" />
<UnusedClass errorLevel="suppress" />
<UndefinedClass errorLevel="suppress" />
<PossiblyUnusedParam errorLevel="suppress" />
<PossiblyUnusedProperty errorLevel="suppress" />
<PossiblyUnusedReturnValue errorLevel="suppress" />
<MissingTemplateParam errorLevel="suppress" />
<UnsupportedPropertyReferenceUsage errorLevel="suppress" />
<MissingOverrideAttribute errorLevel="suppress" />
<MissingPureAnnotation errorLevel="suppress" />
<MissingAbstractPureAnnotation errorLevel="suppress" />
<MissingInterfaceImmutableAnnotation errorLevel="suppress" />
<MissingImmutableAnnotation errorLevel="suppress" />
</issueHandlers>
<forbiddenFunctions>
<function name="var_dump" />
<function name="dd" />
<function name="dump" />
<function name="print_r" />
</forbiddenFunctions>
</psalm>
================================================
FILE: src/Buffer.php
================================================
<?php
namespace AnourValar\Office;
class Buffer implements \Stringable
{
/**
* @var resource
*/
protected readonly mixed $resource;
/**
* @var string
*/
protected readonly string $filename;
/**
* Creates a temporary file from the buffer
*
* @param string $buffer
* @return void
*/
public function __construct(string $buffer)
{
$this->resource = tmpfile();
fwrite($this->resource, $buffer);
$this->filename = stream_get_meta_data($this->resource)['uri'];
}
/**
* @see magic
*
* @return string
*/
public function __toString(): string
{
return $this->filename;
}
/**
* @return void
* @psalm-suppress InaccessibleProperty
*/
public function __destruct()
{
fclose($this->resource); // works with php-fpm, octane, queue
}
}
================================================
FILE: src/DocumentService.php
================================================
<?php
namespace AnourValar\Office;
use AnourValar\Office\Drivers\DocumentInterface;
class DocumentService
{
use \AnourValar\Office\Traits\Parser;
/**
* @var \AnourValar\Office\Drivers\DocumentInterface
*/
protected \AnourValar\Office\Drivers\DocumentInterface $driver;
/**
* @param \AnourValar\Office\Drivers\DocumentInterface $driver
* @return void
*/
public function __construct(DocumentInterface $driver = new \AnourValar\Office\Drivers\ZipDriver())
{
$this->driver = $driver;
}
/**
* Generate a document from the template (document)
*
* @param string|\Stringable $templateFile
* @param mixed $data
* @return \AnourValar\Office\Generated
*/
public function generate(string|\Stringable $templateFile, mixed $data): Generated
{
// Handle with input data
$data = $this->canonizeData($data);
// Open the template
$templateFormat = Format::tryFrom(mb_strtolower(pathinfo($templateFile, PATHINFO_EXTENSION))) ?? Format::Docx;
$driver = $this->driver->load($templateFile, $templateFormat);
// Handle
$driver->replace($data);
// Return
return new Generated($driver);
}
/**
* @param mixed $data
* @return array
*/
protected function canonizeData(mixed $data): array
{
$result = [];
if (is_object($data) && method_exists($data, 'toArray')) {
$data = $data->toArray();
}
foreach ($this->dot($data) as $key => $value) {
$result["[$key]"] = $value;
}
return $result;
}
}
================================================
FILE: src/Drivers/DocumentInterface.php
================================================
<?php
namespace AnourValar\Office\Drivers;
interface DocumentInterface extends SaveInterface, LoadInterface
{
/**
* Replace markers with values
*
* @param array $data
* @return self
*/
public function replace(array $data): self;
}
================================================
FILE: src/Drivers/GridInterface.php
================================================
<?php
namespace AnourValar\Office\Drivers;
interface GridInterface extends SaveInterface
{
/**
* Create new document
*
* @return self
*/
public function create(): self;
/**
* Set data
*
* @param iterable $data
* @return self
*/
public function setGrid(iterable $data): self;
}
================================================
FILE: src/Drivers/LoadInterface.php
================================================
<?php
namespace AnourValar\Office\Drivers;
interface LoadInterface
{
/**
* Load a template with specific format
*
* @param string $file
* @param \AnourValar\Office\Format $format
* @return \AnourValar\Office\Drivers\SaveInterface
*/
public function load(string $file, \AnourValar\Office\Format $format): SaveInterface;
}
================================================
FILE: src/Drivers/MixInterface.php
================================================
<?php
namespace AnourValar\Office\Drivers;
interface MixInterface extends MultiSheetInterface
{
/**
* Set title for an active sheet
*
* @param string $title
* @return self
*/
public function setSheetTitle(string $title): self;
/**
* Get title of an active sheet
*
* @return string
*/
public function getSheetTitle(): string;
/**
* Merge (union) a sheet from another instanceof of driver
*
* @param \AnourValar\Office\Drivers\MixInterface $driver
* @return self
*/
public function mergeDriver(\AnourValar\Office\Drivers\MixInterface $driver): self;
}
================================================
FILE: src/Drivers/MultiSheetInterface.php
================================================
<?php
namespace AnourValar\Office\Drivers;
interface MultiSheetInterface
{
/**
* Set active sheet
*
* @param int $index
* @return self
*/
public function setSheet(int $index): self;
/**
* Get sheets count
*
* @return int
*/
public function getSheetCount(): int;
}
================================================
FILE: src/Drivers/PhpSpreadsheetDriver.php
================================================
<?php
namespace AnourValar\Office\Drivers;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Style\Border;
class PhpSpreadsheetDriver implements SheetsInterface, GridInterface, MixInterface
{
use \AnourValar\Office\Traits\Parser;
/**
* @see \PhpOffice\PhpSpreadsheet\Style\NumberFormat
*
* @var string
*/
public const FORMAT_DATE = 'm/d/yyyy';
public const FORMAT_DATETIME = 'm/d/yyyy h:mm';
/**
* @var string
*/
public const FORMAT_INT = '#,##0';
public const FORMAT_DOUBLE = '#,##0.00';
public const FORMAT_DOUBLE_10 = '#,##########0.0000000000';
public const FORMAT_PERCENTAGE = '0.00%';
/**
* @var \PhpOffice\PhpSpreadsheet\Spreadsheet
*/
public readonly \PhpOffice\PhpSpreadsheet\Spreadsheet $spreadsheet;
/**
* @var int
*/
protected int $sourceActiveSheetIndex;
/**
* @return \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
*/
public function sheet(): \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
{
return $this->spreadsheet->getActiveSheet();
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\GridInterface::create()
* @psalm-suppress InaccessibleProperty
*/
public function create(): self
{
$instance = new static();
$instance->spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$instance->sourceActiveSheetIndex = 0;
$this->readConfiguration($instance);
return $instance;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\LoadInterface::load()
* @psalm-suppress InaccessibleProperty
*/
public function load(string $file, \AnourValar\Office\Format $format): self
{
$instance = new static();
$instance->spreadsheet = IOFactory::createReader($instance->getFormat($format))->load($file);
$instance->sourceActiveSheetIndex = $instance->spreadsheet->getActiveSheetIndex();
$this->readConfiguration($instance);
return $instance;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SaveInterface::save()
*/
public function save(string $file, \AnourValar\Office\Format $format): void
{
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($this->spreadsheet, $this->getFormat($format));
$this->writeConfiguration($writer);
$count = $this->spreadsheet->getSheetCount();
for ($i = 0; $i < $count; $i++) {
$this->spreadsheet->getSheet($i)->setSelectedCells('A1');
}
$this->spreadsheet->setActiveSheetIndex($this->sourceActiveSheetIndex);
if (method_exists($writer, 'writeAllSheets')) {
$writer->writeAllSheets();
}
$writer->save($file);
}
/**
* Clean up
*
* @return void
*/
public function __destruct()
{
if (isset($this->spreadsheet)) {
$this->spreadsheet->disconnectWorksheets();
gc_collect_cycles();
}
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\MultiSheetInterface::setSheet()
*/
public function setSheet(int $index): self
{
$this->spreadsheet->setActiveSheetIndex($index);
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\MultiSheetInterface::getSheetCount()
*/
public function getSheetCount(): int
{
return $this->spreadsheet->getSheetCount();
}
/**
* Apply value to a cell
*
* @param string $cell
* @param mixed $value
* @param bool $autoCellFormat
* @return self
*/
public function setValue(string $cell, $value, bool $autoCellFormat = true): self
{
if ($value instanceof \DateTimeInterface) {
$this->sheet()->setCellValue($cell, \PhpOffice\PhpSpreadsheet\Shared\Date::PHPToExcel($value));
if ($autoCellFormat) {
$this->setCellFormat($cell, static::FORMAT_DATE);
}
} elseif (is_string($value) || is_null($value)) {
if (is_numeric($value)) {
$this->sheet()->getCell($cell)->setValueExplicit($value, DataType::TYPE_STRING);
} else {
$this->sheet()->setCellValue($cell, $value);
}
} else {
if ($autoCellFormat && is_double($value)) {
$this->setCellFormat($cell, static::FORMAT_DOUBLE);
} elseif ($autoCellFormat && is_integer($value)) {
$this->setCellFormat($cell, static::FORMAT_INT);
}
$this->sheet()->getCell($cell)->setValueExplicit($value, DataType::TYPE_NUMERIC);
}
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::setValues()
*/
public function setValues(array $data, bool $autoCellFormat = true): self
{
foreach ($data as $row => $columns) {
foreach ($columns as $column => $value) {
$this->setValue($column.$row, $value, $autoCellFormat);
}
}
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\GridInterface::setGrid()
*/
public function setGrid(iterable $data): self
{
$row = 0;
foreach ($data as $values) {
$row++;
$column = 'A';
foreach ($values as $value) {
if ($value !== '' && $value !== null) {
$this->setValue($column.$row, $value);
}
$column = $this->strIncrement($column);
}
}
return $this;
}
/**
* Get cell' value
*
* @param string $cell
* @return mixed
*/
public function getValue(string $cell)
{
return $this->sheet()->getCell($cell)->getValue();
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::getValues()
*/
public function getValues(?string $ceilRange): array
{
if (! $ceilRange) {
$ceilRange = sprintf('A1:%s%s', $this->sheet()->getHighestColumn(), $this->sheet()->getHighestRow());
}
return $this->sheet()->rangeToArray(
$ceilRange, // The worksheet range that we want to retrieve
null, // Value that should be returned for empty cells
false, // Should formulas be calculated (the equivalent of getCalculatedValue() for each cell)
false, // Should values be formatted (the equivalent of getFormattedValue() for each cell)
true // Should the array be indexed by cell row and cell column
);
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::getMergeCells()
*/
public function getMergeCells(): array
{
return array_values($this->sheet()->getMergeCells());
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::mergeCells()
*/
public function mergeCells(string $ceilRange): self
{
$this->sheet()->mergeCells($ceilRange);
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::copyStyle()
*/
public function copyStyle(string $cellFrom, string $rangeTo): self
{
$this->sheet()->duplicateStyle($this->sheet()->getStyle($cellFrom), $rangeTo);
if ($conditionalStyle = $this->sheet()->getConditionalStyles($cellFrom)) {
$this->sheet()->duplicateConditionalStyle($conditionalStyle, $rangeTo);
}
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::copyCellFormat()
*/
public function copyCellFormat(string $cellFrom, string $rangeTo): self
{
$this->setCellFormat($rangeTo, $this->sheet()->getStyle($cellFrom)->getNumberFormat()->getFormatCode());
return $this;
}
/**
* Set cell (data) format
*
* @param string $range
* @param string $format
* @return self
*/
public function setCellFormat(string $range, string $format): self
{
$this->sheet()->getStyle($range)->getNumberFormat()->setFormatCode($format);
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::addRow()
*/
public function addRow(int $rowBefore, int $qty = 1): self
{
$this->sheet()->insertNewRowBefore($rowBefore, $qty);
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::deleteRow()
*/
public function deleteRow(int $row, int $qty = 1): self
{
foreach ($this->getMergeCells() as $merge) {
preg_match('#(\d+)#', $merge, $details);
if ($details[1] == $row) {
$this->sheet()->unmergeCells($merge);
}
}
$this->sheet()->removeRow($row, $qty);
return $this;
}
/**
* Add a column
*
* @param string $columnBefore
* @param int $qty
* @return self
*/
public function addColumn(string $columnBefore, int $qty = 1): self
{
$this->sheet()->insertNewColumnBefore($columnBefore, $qty);
return $this;
}
/**
* Set auto-width for a column
*
* @param string $column
* @return self
*/
public function autoWidth(string $column): self
{
$this->sheet()->getColumnDimension($column)->setAutoSize(true);
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SheetsInterface::copyWidth()
*/
public function copyWidth(string $columnFrom, string $columnTo): self
{
$width = $this->sheet()->getColumnDimension($columnFrom)->getWidth();
$this->setWidth($columnTo, $width);
return $this;
}
/**
* Set fixed width for a column
*
* @param string $column
* @param int|float $width
* @return self
*/
public function setWidth(string $column, int|float $width): self
{
$this->sheet()->getColumnDimension($column)->setWidth($width);
return $this;
}
/**
* Copy row's height
*
* @param int $rowFrom
* @param int $rowTo
* @return self
*/
public function copyHeight(int $rowFrom, int $rowTo): self
{
$height = $this->sheet()->getRowDimension($rowFrom)->getRowHeight();
$this->setHeight($rowTo, $height);
return $this;
}
/**
* Set fixed height for a row
*
* @param string $row
* @param int|float $height
* @return self
*/
public function setHeight(string $row, int|float $height): self
{
$this->sheet()->getRowDimension($row)->setRowHeight($height);
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\MixInterface::setSheetTitle()
*/
public function setSheetTitle(string $title): self
{
$this->sheet()->setTitle($title);
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\MixInterface::getSheetTitle()
*/
public function getSheetTitle(): string
{
return $this->sheet()->getTitle();
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\MixInterface::mergeDriver()
*/
public function mergeDriver(\AnourValar\Office\Drivers\MixInterface $driver): self
{
$index = $driver->spreadsheet->getActiveSheetIndex();
$this->spreadsheet->addExternalSheet($driver->sheet());
$driver->spreadsheet->createSheet($index);
return $this;
}
/**
* Apply cell`s style without format
*
* @param string $cellFrom
* @param string $rangeTo
* @param bool $copyAlignment
* @return self
*/
public function copyStyleWithoutFormat(string $cellFrom, string $rangeTo, bool $copyAlignment = false): self
{
$style = $this->sheet()->getStyle($cellFrom)->exportArray();
if (! $copyAlignment) {
unset($style['alignment'], $style['numberFormat'], $style['protection']);
} else {
unset($style['numberFormat'], $style['protection']);
}
// @TODO: fixed ?
if (
! isset($style['borders']['allBorders'])
&& isset($style['borders']['bottom'], $style['borders']['top'])
&& isset($style['borders']['left'], $style['borders']['right'])
&& $style['borders']['bottom'] == $style['borders']['top']
&& $style['borders']['bottom'] == $style['borders']['left']
&& $style['borders']['bottom'] == $style['borders']['right']
) {
$style['borders']['allBorders'] = $style['borders']['bottom'];
unset($style['borders']['bottom'], $style['borders']['top']);
unset($style['borders']['left'], $style['borders']['right']);
}
$this->sheet()->getStyle($rangeTo)->applyFromArray($style);
return $this;
}
/**
* Find a cell with the value
*
* @param mixed $value
* @param bool $strict
* @return array|null
*/
public function findCell($value, bool $strict = false): ?array
{
foreach ($this->getValues(null) as $row => $rowData) {
foreach ($rowData as $column => $columnData) {
if (($strict && $columnData === $value) || (! $strict && $columnData == $value)) {
return [$column, $row];
}
}
}
return null;
}
/**
* Duplicate rows (with style, value) by range
*
* @param string $ceilRange
* @param callable $value
* @param int $indentRows
* @param bool $addRows
* @return self
*/
public function duplicateRows(string $ceilRange, callable $value, int $indentRows = 0, bool $addRows = true): self
{
$range = explode(':', $ceilRange);
$range[0] = preg_split('#([A-Z]+)([\d]+)#S', $range[0], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$range[1] = preg_split('#([A-Z]+)([\d]+)#S', $range[1], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$shift = $range[1][1] - $range[0][1] + 1 + $indentRows;
$mergeCells = $this->getMergeCells();
$values = $this->getValues($ceilRange);
// Rows
if ($addRows) {
$this->addRow($range[1][1] + 1, $shift + $indentRows);
}
// Merge
foreach ($mergeCells as $item) {
$item = explode(':', $item);
$item[0] = preg_split('#([A-Z]+)([\d]+)#S', $item[0], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$item[1] = preg_split('#([A-Z]+)([\d]+)#S', $item[1], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
if (
$item[0][1] >= $range[0][1] && $item[0][1] <= $range[1][1] // rows
&& $item[1][1] >= $range[0][1] && $item[1][1] <= $range[1][1]
&& $this->isColumnGE($item[0][0], $range[0][0]) && $this->isColumnLE($item[0][0], $range[1][0]) // columns
&& $this->isColumnGE($item[1][0], $range[0][0]) && $this->isColumnLE($item[1][0], $range[1][0])
) {
$this->mergeCells($item[0][0].($item[0][1] + $shift) . ':' . $item[1][0].($item[1][1] + $shift));
}
}
$curr = $range[0][1];
while ($curr <= $range[1][1]) { // rows
// Height
$this->copyHeight($curr, $curr + $shift);
// Style, CellFormat, Value
$column = $range[0][0];
while ($this->isColumnLE($column, $range[1][0])) {
$this->copyStyle($column . $curr, $column . ($curr + $shift));
$this->copyCellFormat($column . $curr, $column . ($curr + $shift));
if (isset($values[$curr][$column])) {
$this->setValue($column . ($curr + $shift), $value($values[$curr][$column], $column, $curr), false);
}
$column = $this->strIncrement($column);
}
$curr++;
}
return $this;
}
/**
* Set custom style for the range of cells
*
* @param string $range
* @param array $style
* @return self
*/
public function setStyle(string $range, array $style): self
{
if (isset($style['bold'])) {
$this->sheet()->getStyle($range)->getFont()->setBold($style['bold']);
}
if (isset($style['italic'])) {
$this->sheet()->getStyle($range)->getFont()->setItalic($style['italic']);
}
if (isset($style['size'])) {
$this->sheet()->getStyle($range)->getFont()->setSize($style['size']);
}
if (isset($style['underline'])) {
$this->sheet()->getStyle($range)->getFont()->setUnderline($style['underline']);
}
if (isset($style['color'])) {
$this->sheet()->getStyle($range)->getFont()->getColor()->setRGB($style['color']);
}
if (isset($style['background_color'])) {
$this
->sheet()
->getStyle($range)
->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->getStartColor()
->setRGB($style['background_color']);
}
if (isset($style['borders'])) {
$this
->sheet()
->getStyle($range)
->getBorders()
->getAllBorders()
->setBorderStyle($style['borders'] ? Border::BORDER_THIN : Border::BORDER_NONE);
}
if (isset($style['borders_outline'])) {
$this
->sheet()
->getStyle($range)
->getBorders()
->getOutline()
->setBorderStyle($style['borders_outline'] ? Border::BORDER_THIN : Border::BORDER_NONE);
}
if (isset($style['align'])) {
$align = match ($style['align']) {
\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_LEFT => 'left',
\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER => 'center',
\PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_RIGHT => 'right',
};
$this
->sheet()
->getStyle($range)
->getAlignment()->setHorizontal($align);
}
if (isset($style['valign'])) {
$valign = match ($style['valign']) {
\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_TOP => 'top',
\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_CENTER => 'center',
\PhpOffice\PhpSpreadsheet\Style\Alignment::VERTICAL_BOTTOM => 'bottom',
};
$this
->sheet()
->getStyle($range)
->getAlignment()->setVertical($valign);
}
if (isset($style['wrap'])) {
$this
->sheet()
->getStyle($range)
->getAlignment()->setWrapText($style['wrap']);
}
return $this;
}
/**
* Place an image
*
* @param string $filename
* @param string $cell
* @param array $options
* @return self
*/
public function insertImage(string $filename, string $cell, array $options = []): self
{
$drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
if (isset($options['base64'])) {
$filename = 'data:image/' . $options['base64'] . ';base64,' . base64_encode(file_get_contents($filename));
}
$drawing->setPath($filename);
$drawing->setCoordinates($cell);
if (isset($options['coordinates2'])) {
$drawing->setCoordinates2($options['coordinates2']);
}
if (isset($options['name'])) {
$drawing->setName($options['name']);
}
if (isset($options['offset_x'])) {
$drawing->setOffsetX($options['offset_x']);
}
if (isset($options['offset_x2'])) {
$drawing->setOffsetX2($options['offset_x2']);
}
if (isset($options['offset_y'])) {
$drawing->setOffsetY($options['offset_y']);
}
if (isset($options['offset_y2'])) {
$drawing->setOffsetY2($options['offset_y2']);
}
if (isset($options['rotation'])) {
$drawing->setRotation($options['rotation']);
}
if (isset($options['width']) && isset($options['height'])) {
$drawing
->setResizeProportional(false)
->setWidth($options['width'])
->setHeight($options['height']);
} elseif (isset($options['width'])) {
$drawing->setWidth($options['width']);
} elseif (isset($options['height'])) {
$drawing->setHeight($options['height']);
}
$drawing->setWorksheet($this->sheet());
return $this;
}
/**
* "Reader" configuration
*
* @param \AnourValar\Office\Drivers\PhpSpreadsheetDriver $instance
* @return void
*/
protected function readConfiguration(PhpSpreadsheetDriver $instance): void
{
//
}
/**
* "Writer" configuration
*
* @param \PhpOffice\PhpSpreadsheet\Writer\IWriter $writer
* @return void
*/
protected function writeConfiguration(\PhpOffice\PhpSpreadsheet\Writer\IWriter $writer): void
{
if ($writer instanceof \PhpOffice\PhpSpreadsheet\Writer\Csv) {
$writer->setDelimiter(';')->setUseBOM(true);
}
}
/**
* @param \AnourValar\Office\Format $format
* @return string
*/
protected function getFormat(\AnourValar\Office\Format $format): string
{
return match ($format) {
\AnourValar\Office\Format::Xlsx => 'Xlsx',
\AnourValar\Office\Format::Pdf => 'Mpdf',
\AnourValar\Office\Format::Html => 'Html',
\AnourValar\Office\Format::Ods => 'Ods',
\AnourValar\Office\Format::Csv => 'Csv',
default => throw new \RuntimeException('Format is not supported.'),
};
}
}
================================================
FILE: src/Drivers/SaveInterface.php
================================================
<?php
namespace AnourValar\Office\Drivers;
interface SaveInterface
{
/**
* Save in specific format
*
* @param string $file
* @param \AnourValar\Office\Format $format
* @return void
*/
public function save(string $file, \AnourValar\Office\Format $format): void;
}
================================================
FILE: src/Drivers/SheetsInterface.php
================================================
<?php
namespace AnourValar\Office\Drivers;
interface SheetsInterface extends SaveInterface, LoadInterface, MultiSheetInterface
{
/**
* Set values
*
* @param array $data
* @param bool $autoCellFormat
* @return self
*/
public function setValues(array $data, bool $autoCellFormat = true): self;
/**
* Get values (range)
*
* @param string|null $ceilRange
* @return array
*/
public function getValues(?string $ceilRange): array;
/**
* Get merge cells (whole sheet)
*
* @return array
*/
public function getMergeCells(): array;
/**
* Merge cells
*
* @param string $ceilRange
* @return self
*/
public function mergeCells(string $ceilRange): self;
/**
* Apply cell`s style
*
* @param string $cellFrom
* @param string $rangeTo
* @return self
*/
public function copyStyle(string $cellFrom, string $rangeTo): self;
/**
* Copy cell`s format
*
* @param string $cellFrom
* @param string $rangeTo
* @return self
*/
public function copyCellFormat(string $cellFrom, string $rangeTo): self;
/**
* Add a row
*
* @param int $rowBefore
* @param int $qty
* @return self
*/
public function addRow(int $rowBefore, int $qty = 1): self;
/**
* Delete a row
*
* @param int $row
* @param int $qty
* @return self
*/
public function deleteRow(int $row, int $qty = 1): self;
/**
* Copy column's width
*
* @param string $columnFrom
* @param string $columnTo
* @return self
*/
public function copyWidth(string $columnFrom, string $columnTo): self;
}
================================================
FILE: src/Drivers/ZipDriver.php
================================================
<?php
namespace AnourValar\Office\Drivers;
class ZipDriver implements DocumentInterface, GridInterface
{
use \AnourValar\Office\Traits\Parser;
use \AnourValar\Office\Traits\XFormat;
/**
* @var \AnourValar\Office\Format
*/
protected readonly \AnourValar\Office\Format $format;
/**
* @var array
*/
protected array $fileSystem;
/**
* @var array
*/
protected array $gridOptions = [];
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\GridInterface::create()
*/
public function create(): self
{
return $this->load(__DIR__ . '/../resources/grid.xlsx', \AnourValar\Office\Format::Xlsx);
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\LoadInterface::load()
* @psalm-suppress InaccessibleProperty
*/
public function load(string $file, \AnourValar\Office\Format $format): self
{
if (! in_array($format, [\AnourValar\Office\Format::Docx, \AnourValar\Office\Format::Xlsx])) {
throw new \LogicException('Driver only supports Docx, Xlsx formats.');
}
$instance = new static();
$fileSystem = [];
$zipArchive = new \ZipArchive();
$zipArchive->open($file);
try {
$count = $zipArchive->numFiles;
for ($i = 0; $i < $count; $i++) {
$filename = $zipArchive->getNameIndex($i);
$content = $zipArchive->getFromName($filename);
$fileSystem[$filename] = $content;
}
} catch (\Throwable $e) {
$zipArchive->close();
throw $e;
}
$zipArchive->close();
$instance->fileSystem = $fileSystem;
$instance->format = $format;
return $instance;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\SaveInterface::save()
*/
public function save(string $file, \AnourValar\Office\Format $format): void
{
if ($format != $this->format) {
throw new \LogicException('Driver only supports saving in the same format.');
}
if (class_exists(\ZipStream\Option\Archive::class)) {
$zipStream = new \ZipStream\ZipStream(); // 2.x
} else {
$zipStream = new \ZipStream\ZipStream(sendHttpHeaders: false, defaultEnableZeroHeader: false); // 3.x
}
ob_start();
try {
foreach ($this->fileSystem as $filename => $content) {
$zipStream->addFile($filename, $content);
}
} catch (\Throwable $e) {
$zipStream->finish();
ob_get_clean();
throw $e;
}
$zipStream->finish();
file_put_contents($file, ob_get_clean());
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\DocumentInterface::replace()
*/
public function replace(array $data): self
{
foreach ($data as &$value) {
$value = $this->escape($value);
}
unset($value);
foreach ($this->fileSystem as $filename => &$content) {
if ($content && mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'xml') {
$content = $this->handleReplace($content, $data);
}
}
unset($content);
return $this;
}
/**
* {@inheritDoc}
* @see \AnourValar\Office\Drivers\GridInterface::setGrid()
*/
public function setGrid(iterable $data): self
{
$sheet = ''; // matrix
$cols = ''; // columns
$buckets = []; // text (sharedStrings)
$bucketsIndex = 0;
$row = 0; // rows count
$columnsCount = 0; // columns count
$firstColumn = 'A'; // columns shift
// Styles
$styles = $this->loadGridStyles();
// Head (titles)
while ($data->valid()) {
$row++;
$titles = $data->current();
$data->next();
$columnsCount = count($titles);
if (! $titles) {
continue;
}
$sheet .= '<row r="'.$row.'" spans="1:'.$columnsCount.'" ht="'.($this->gridOptions['height'][$row] ?? 25).'" customHeight="1" x14ac:dyDescent="0.3">';
$column = 'A';
foreach ($titles as $value) {
$value = (string) $value;
if ($value === '') {
$firstColumn = $this->strIncrement($firstColumn);
$column = $this->strIncrement($column);
continue;
}
$value = $this->escape($value);
$curr = $buckets[$value] ?? null;
if ($curr === null) {
$curr = $bucketsIndex;
$buckets[$value] = $curr;
$bucketsIndex++;
}
$sheet .= '<c r="'.$column.$row.'" t="s" s="'.$styles['header'].'"><v>'.$curr.'</v></c>';
$column = $this->strIncrement($column);
}
$sheet .= '</row>';
break;
}
// Custom styles
foreach (($this->gridOptions['style'] ?? []) as $column => $alias) {
if (isset($styles[$alias])) {
$styles[$column] = $styles[$alias];
}
}
// Body (data)
while ($data->valid()) {
$values = $data->current();
$data->next();
$row++;
$ht = '';
if (isset($this->gridOptions['height'][$row])) {
$ht = 'ht="'.$this->gridOptions['height'][$row].'" customHeight="1" ';
}
$sheet .= '<row r="'.$row.'" '.$ht.'spans="1:'.$columnsCount.'" x14ac:dyDescent="0.3">';
$column = 'A';
foreach ($values as $value) {
if ($value instanceof \Stringable && ! $value instanceof \DateTimeInterface) {
$value = (string) $value;
}
if ($value === null || $value === '') {
if ($this->isColumnGE($column, $firstColumn)) {
$sheet .= '<c r="'.$column.$row.'" s="'.($styles[$column] ?? $styles['string']).'"/>';
}
} elseif (is_string($value)) {
$value = $this->escape($value);
$curr = $buckets[$value] ?? null;
if ($curr === null) {
$curr = $bucketsIndex;
$buckets[$value] = $curr;
$bucketsIndex++;
}
$style = ($styles[$column] ?? $styles['string']);
$sheet .= '<c r="'.$column.$row.'" t="s" s="'.$style.'"><v>'.$curr.'</v></c>';
} elseif (is_double($value)) {
$style = ($styles[$column] ?? $styles['double']);
$sheet .= '<c r="'.$column.$row.'" s="'.$style.'"><v>'.$value.'</v></c>';
} elseif (is_integer($value)) {
$style = ($styles[$column] ?? $styles['integer']);
$sheet .= '<c r="'.$column.$row.'" s="'.$style.'"><v>'.$value.'</v></c>';
} elseif ($value instanceof \DateTimeInterface) {
$style = ($styles[$column] ?? $styles['date']);
$sheet .= '<c r="'.$column.$row.'" s="'.$style.'"><v>'.$this->excelDate($value).'</v></c>';
} else {
throw new \RuntimeException('Unsupported type of value.');
}
$column = $this->strIncrement($column);
}
$sheet .= '</row>';
}
// Columns
$column = 'A';
for ($index = 1; $index <= $columnsCount; $index++) {
if ($this->isColumnGE($column, $firstColumn)) {
$width = ($this->gridOptions['width'][$column] ?? 20);
$cols .= '<col min="'.$index.'" max="'.$index.'" width="'.$width.'" customWidth="1"/>';
}
$column = $this->strIncrement($column);
}
// Save buckets
$this->saveGridSharedStrings($buckets);
// Save columns & matrix
$this->saveGridWorksheet($cols, $sheet, $row, $column);
// Etc
$this->saveGridEtc();
return $this;
}
/**
* @param string $content
* @param array $data
* @return string
*/
protected function handleReplace(string $content, array &$data): string
{
foreach ($data as $from => $to) {
$pattern = mb_str_split($from);
foreach ($pattern as &$patternItem) {
$patternItem = preg_quote($patternItem);
$patternItem .= '(\<[^\[]*)?';
}
unset($patternItem);
$pattern = implode('', $pattern);
$content = preg_replace_callback("#$pattern#Uu", function ($patterns) use ($from, $to) {
if (strip_tags($patterns[0]) == $from) {
return $to;
}
return $patterns[0];
}, $content);
}
return $content;
}
/**
* Set styles map for the grid template [header, integer, double, double_10, string, date, percentage, ...]
*
* @param string $column
* @param string $style
* @return self
*/
public function setStyle(string $column, string $style): self
{
$this->gridOptions['style'][$column] = $style;
return $this;
}
/**
* Set column's width for the grid
*
* @param string $column
* @param int $width
* @return self
*/
public function setWidth(string $column, int $width): self
{
$this->gridOptions['width'][$column] = $width;
return $this;
}
/**
* Set row's height for the grid
*
* @param string $row
* @param int|float $height
* @return self
*/
public function setHeight(string $row, int|float $height): self
{
$this->gridOptions['height'][$row] = $height;
return $this;
}
/**
* Set sheet title for the grid
*
* @param string $title
* @return self
*/
public function setSheetTitle(string $title): self
{
$this->fileSystem['xl/workbook.xml'] = preg_replace(
'#\<sheet name="(.+)" sheetId\="(.+)" r\:id\="(.+)"\/\>#uU',
'<sheet name="'.$this->escape($title).'" sheetId="$2" r:id="$3"/>',
$this->fileSystem['xl/workbook.xml']
);
return $this;
}
/**
* @return array
*/
protected function loadGridStyles(): array
{
$styles = [];
// Bucket
preg_match_all('#<si><t>(.*)</t></si>#uU', $this->fileSystem['xl/sharedStrings.xml'], $buckets);
$buckets = $buckets[1];
// Matrix
preg_match_all(
'#<c r="[A-Z\d]+" s="(\d+)" t="s"><v>(\d+)</v></c>#uU',
$this->fileSystem['xl/worksheets/sheet1.xml'],
$matrix
);
// Parse the map
foreach ($matrix[2] as $key => $value) {
if (isset($buckets[$value])) {
$styles[mb_substr($buckets[$value], 1, -1)] = $matrix[1][$key];
}
}
// Presets
return array_merge(['header' => 1, 'string' => 1, 'double' => 1, 'integer' => 1, 'date' => 1], $styles);
}
/**
* @param array $buckets
* @return void
*/
protected function saveGridSharedStrings(array &$buckets): void
{
$sst = '';
foreach (array_keys($buckets) as $word) {
$sst .= '<si><t>'.$word.'</t></si>';
}
$count = count($buckets);
$this->fileSystem['xl/sharedStrings.xml'] = <<<HERE
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="$count" uniqueCount="$count">
$sst
</sst>
HERE;
}
/**
* @param string $cols
* @param string $sheet
* @param int $lastRow
* @param string $lastColumn
* @return void
*/
protected function saveGridWorksheet(string &$cols, string &$sheet, int $lastRow, string $lastColumn): void
{
$worksheet = 'xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" '
. 'xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" '
. 'xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" '
. 'mc:Ignorable="x14ac xr xr2 xr3" '
. 'xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" '
. 'xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" '
. 'xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" '
. 'xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" '
. 'xr:uid="{9CD2C5FF-272A-44F0-88FE-0A0D7B1A3B22}"';
if ($cols) {
$cols = "<cols>$cols</cols>";
}
if ($sheet) {
$sheet = "<sheetData>$sheet</sheetData>";
} else {
$sheet = '<sheetData/>';
}
if (! $lastRow) {
$lastRow = 1;
}
$this->fileSystem['xl/worksheets/sheet1.xml'] = <<<HERE
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<worksheet $worksheet>
<dimension ref="A1:{$lastColumn}{$lastRow}"/>
<sheetViews>
<sheetView tabSelected="1" workbookViewId="0"><selection activeCell="A1" sqref="A1"/></sheetView>
</sheetViews>
<sheetFormatPr defaultRowHeight="14.4" x14ac:dyDescent="0.3"/>
$cols
$sheet
<pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>
<pageSetup paperSize="9" orientation="portrait" horizontalDpi="1200" verticalDpi="1200" r:id="rId1"/>
</worksheet>
HERE;
}
/**
* @return void
*/
protected function saveGridEtc(): void
{
// Created at timestamp
$this->fileSystem['docProps/core.xml'] = preg_replace(
'#(\<dcterms\:created xsi\:type\="[^"]+">)(.*?)(\<\/dcterms\:created\>)#',
'${1}' . date('Y-m-d\TH:i:s\Z') . '$3',
$this->fileSystem['docProps/core.xml']
);
// Modified at timestamp
$this->fileSystem['docProps/core.xml'] = preg_replace(
'#(\<dcterms\:modified xsi\:type\="[^"]+">)(.*?)(\<\/dcterms\:modified\>)#',
'${1}' . date('Y-m-d\TH:i:s\Z') . '$3',
$this->fileSystem['docProps/core.xml']
);
}
}
================================================
FILE: src/Facades/ExportGridInterface.php
================================================
<?php
namespace AnourValar\Office\Facades;
use AnourValar\Office\Drivers\GridInterface;
interface ExportGridInterface
{
/**
* Sheet title
*
* @param array $request
* @return string
*/
public function sheetTitle(array $request): string;
/**
* Columns structure
*
* @param array $request
* @return array
*/
public function columns(array $request): array;
/**
* Row iteration
*
* @param mixed $row
* @param \AnourValar\Office\Drivers\GridInterface $driver
* @param int $rowNumber
* @param array $request
* @return array
*/
public function item($row, GridInterface $driver, int $rowNumber, array $request): array;
/**
* Filename
*
* @param string $ext
* @param array $request
* @return string
*/
public function fileName(string $ext, array $request): string;
}
================================================
FILE: src/Facades/ExportGridQueryInterface.php
================================================
<?php
namespace AnourValar\Office\Facades;
/**
* Usage example:
*
* if (! in_array($format, [\AnourValar\Office\Format::Xlsx, \AnourValar\Office\Format::Csv])) {
* throw new \App\Exceptions\ValidationException('Format is not supported.');
* }
*
* $generatorData = $this->buildBy($myGrid->query()->acl(), array_replace($this->profile, $this->profileExport)); // out of context
* $request = $this->getBuildRequest()->get();
*
* return response()->streamDownload(
* function () use ($generatorData, $myGrid, $exportService, $format, $request) {
* echo $exportService->grid($generatorData, $myGrid, $format, $request);
* },
* $myGrid->fileName($format->fileExtension(), $request),
* ['Access-Control-Expose-Headers' => 'Content-Disposition']
* );
*/
interface ExportGridQueryInterface extends ExportGridInterface
{
/**
* Laravel's Query builder (base query)
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function query(): \Illuminate\Database\Eloquent\Builder;
}
================================================
FILE: src/Facades/ExportService.php
================================================
<?php
namespace AnourValar\Office\Facades;
use AnourValar\Office\Drivers\GridInterface;
use AnourValar\Office\Format;
class ExportService
{
/**
* Extra options
*
* @var string
*/
public const PERCENTAGE = 'percentage';
public const DOUBLE_10 = 'double_10';
public const DATETIME = 'datetime';
/**
* Generate a grid
*
* @param \Closure $dataGenerator
* @param \AnourValar\Office\Facades\ExportGridInterface $grid
* @param \AnourValar\Office\Format $format
* @param array $request
* @return string
*/
public function grid(\Closure $dataGenerator, ExportGridInterface $grid, Format $format = Format::Xlsx, array $request = []): string
{
$extras = [];
return (new \AnourValar\Office\GridService($this->getDriver($format)))
->hookHeader(function (GridInterface $driver, mixed $header, string|int $key, string $column, int $rowNumber) use (&$extras) {
if (isset($header['width'])) {
$driver->setWidth($column, $header['width']);
}
if (isset($header['height'])) {
$driver->setHeight($rowNumber, $header['height']);
}
$extras = array_merge($extras, $this->handleExtras($driver, $column, $header));
return $header['title'];
})
->hookRow(function (GridInterface $driver, mixed $row, string|int $key, int $rowNumber) use ($grid, $request) {
return $grid->item($row, $driver, $rowNumber, $request);
})
->hookAfter(function (
GridInterface $driver,
?string $headersRange,
?string $dataRange,
?string $totalRange,
array $columns
) use ($grid, $request, &$extras) {
$driver->setSheetTitle($grid->sheetTitle($request));
foreach ($extras as $extra) {
$extra();
}
})
->generate($grid->columns($request), $dataGenerator)
->save($format);
}
/**
* @param \AnourValar\Office\Drivers\GridInterface $driver
* @param string $column
* @param array $header
* @return array
* @throws \RuntimeException
*/
protected function handleExtras(GridInterface $driver, string $column, array $header): array
{
$extras = [];
// percentage
if (! empty($header[self::PERCENTAGE])) {
if ($driver instanceof \AnourValar\Office\Drivers\ZipDriver) {
$driver->setStyle($column, 'percentage');
} elseif ($driver instanceof \AnourValar\Office\Drivers\PhpSpreadsheetDriver) {
$extras[] = fn () => $driver->setCellFormat($column, \AnourValar\Office\Drivers\PhpSpreadsheetDriver::FORMAT_PERCENTAGE);
} else {
throw new \RuntimeException('The driver does not support the "percentage" feature.');
}
}
// double_10
if (! empty($header[self::DOUBLE_10])) {
if ($driver instanceof \AnourValar\Office\Drivers\ZipDriver) {
$driver->setStyle($column, 'double_10');
} elseif ($driver instanceof \AnourValar\Office\Drivers\PhpSpreadsheetDriver) {
$extras[] = fn () => $driver->setCellFormat($column, \AnourValar\Office\Drivers\PhpSpreadsheetDriver::FORMAT_DOUBLE_10);
} else {
throw new \RuntimeException('The driver does not support the "double_10" feature.');
}
}
// datetime
if (! empty($header[self::DATETIME])) {
if ($driver instanceof \AnourValar\Office\Drivers\ZipDriver) {
$driver->setStyle($column, 'datetime');
} elseif ($driver instanceof \AnourValar\Office\Drivers\PhpSpreadsheetDriver) {
$extras[] = fn () => $driver->setCellFormat($column, \AnourValar\Office\Drivers\PhpSpreadsheetDriver::FORMAT_DATETIME);
} else {
throw new \RuntimeException('The driver does not support the "datetime" feature.');
}
}
return $extras;
}
/**
* @param \AnourValar\Office\Format $format
* @return \AnourValar\Office\Drivers\GridInterface
*/
protected function getDriver(Format $format): GridInterface
{
if ($format == Format::Xlsx) {
return new \AnourValar\Office\Drivers\ZipDriver();
}
return new \AnourValar\Office\Drivers\PhpSpreadsheetDriver();
}
}
================================================
FILE: src/Format.php
================================================
<?php
namespace AnourValar\Office;
enum Format: string
{
case Xlsx = 'xlsx'; // sheets | grid => reader + writer
case Pdf = 'pdf'; // sheets | grid => writer
case Html = 'html'; // sheets | grid => reader + writer
case Ods = 'ods'; // sheets | grid => reader + writer
case Csv = 'csv'; // sheets | grid => reader + writer
case Docx = 'docx'; // document => reader + writer
/**
* @return string
*/
public function fileExtension(): string
{
return match ($this) {
Format::Xlsx => 'xlsx',
Format::Pdf => 'pdf',
Format::Html => 'html',
Format::Ods => 'ods',
Format::Csv => 'csv',
Format::Docx => 'docx',
};
}
/**
* MIME
*
* @return string
*/
public function contentType(): string
{
return match ($this) {
Format::Xlsx => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
Format::Pdf => 'application/pdf',
Format::Html => 'text/html',
Format::Ods => 'application/vnd.oasis.opendocument.spreadsheet',
Format::Csv => 'text/csv',
Format::Docx => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
};
}
}
================================================
FILE: src/Generated.php
================================================
<?php
namespace AnourValar\Office;
class Generated
{
/**
* @var \AnourValar\Office\Drivers\SaveInterface
*/
public readonly \AnourValar\Office\Drivers\SaveInterface $driver;
/**
* Handle template's saving
*
* @var \Closure(\AnourValar\Office\Drivers\SaveInterface $driver, \AnourValar\Office\Format $format): void
*/
protected ?\Closure $hookSave = null;
/**
* @param \AnourValar\Office\Drivers\SaveInterface $driver
* @return void
*/
public function __construct(\AnourValar\Office\Drivers\SaveInterface $driver)
{
$this->driver = $driver;
}
/**
* Save generated document to the buffer
*
* @param \AnourValar\Office\Format $format
* @return string
*/
public function save(Format $format): string
{
ob_start();
if ($this->hookSave) {
($this->hookSave)($this->driver, $format);
} else {
$this->driver->save('php://output', $format);
}
return ob_get_clean();
}
/**
* Save generated document to the file
*
* @param string $filename
* @param \AnourValar\Office\Format|null $format
* @return int|null
*/
public function saveAs(string $filename, ?Format $format = null): ?int
{
if (! $format) {
$format = Format::from(mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION)));
}
return file_put_contents($filename, $this->save($format));
}
/**
* Set hookSave
*
* @param \Closure|null $closure
* @return self
*/
public function hookSave(?\Closure $closure): self
{
$this->hookSave = $closure;
return $this;
}
}
================================================
FILE: src/GridService.php
================================================
<?php
namespace AnourValar\Office;
use AnourValar\Office\Drivers\GridInterface;
class GridService
{
use \AnourValar\Office\Traits\Parser;
/**
* @var \AnourValar\Office\Drivers\GridInterface
*/
protected \AnourValar\Office\Drivers\GridInterface $driver;
/**
* Handle template's creating
*
* @var \Closure(GridInterface $driver): GridInterface
*/
protected ?\Closure $hookLoad = null;
/**
* Actions with template before data inserted
*
* @var \Closure(GridInterface $driver, array &$headers, iterable &$data, string $leftTopCorner): void
*/
protected ?\Closure $hookBefore = null;
/**
* Header handler
*
* @var \Closure(GridInterface $driver, mixed $header, string|int $key, string $column, int $rowNumber): string
*/
protected ?\Closure $hookHeader = null;
/**
* Row data handler
*
* @var \Closure(GridInterface $driver, mixed $row, string|int $key, int $rowNumber): array
*/
protected ?\Closure $hookRow = null;
/**
* Actions with template after data inserted
*
* @var \Closure(GridInterface $driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns): void
*/
protected ?\Closure $hookAfter = null;
/**
* @param \AnourValar\Office\Drivers\GridInterface $driver
* @return void
*/
public function __construct(GridInterface $driver = new \AnourValar\Office\Drivers\PhpSpreadsheetDriver())
{
$this->driver = $driver;
}
/**
* Generate a document from the template (grid)
*
* @param array $headers
* @param iterable|\Closure $data
* @param string $leftTopCorner
* @return \AnourValar\Office\Generated
*/
public function generate(array $headers, iterable|\Closure $data, string $leftTopCorner = 'A1'): Generated
{
// Handle with data
if ($data instanceof \Closure) {
$data = $data();
}
// Create new document
if ($this->hookLoad) {
$driver = ($this->hookLoad)($this->driver);
if ($driver instanceof Generated) {
$driver = $driver->driver;
}
} else {
$driver = $this->driver->create();
}
// Hook: before
if ($this->hookBefore) {
($this->hookBefore)($driver, $headers, $data, $leftTopCorner);
}
// Set data
$driver->setGrid(
$this->getGenerator($driver, $headers, $data, $leftTopCorner, $headersRange, $dataRange, $totalRange, $columns)()
);
// Hook: after
if ($this->hookAfter) {
($this->hookAfter)($driver, $headersRange, $dataRange, $totalRange, $columns);
}
return new Generated($driver);
}
/**
* Set hookLoad
*
* @param ?\Closure $closure
* @return self
*/
public function hookLoad(?\Closure $closure): self
{
$this->hookLoad = $closure;
return $this;
}
/**
* Set hookBefore
*
* @param ?\Closure $closure
* @return self
*/
public function hookBefore(?\Closure $closure): self
{
$this->hookBefore = $closure;
return $this;
}
/**
* Set hookHeader
*
* @param ?\Closure $closure
* @return self
*/
public function hookHeader(?\Closure $closure): self
{
$this->hookHeader = $closure;
return $this;
}
/**
* Set hookRow
*
* @param ?\Closure $closure
* @return self
*/
public function hookRow(?\Closure $closure): self
{
$this->hookRow = $closure;
return $this;
}
/**
* Set hookAfter
*
* @param ?\Closure $closure
* @return self
*/
public function hookAfter(?\Closure $closure): self
{
$this->hookAfter = $closure;
return $this;
}
/**
* @param \AnourValar\Office\Drivers\GridInterface $driver
* @param array $headers
* @param iterable $data
* @param string $leftTopCorner
* @param mixed $headersRange
* @param mixed $dataRange
* @param mixed $totalRange
* @param mixed $columns
* @return \Closure
* @psalm-suppress UnusedForeachValue
*/
protected function getGenerator(
\AnourValar\Office\Drivers\GridInterface $driver,
array &$headers,
iterable &$data,
string $leftTopCorner,
&$headersRange = null,
&$dataRange = null,
&$totalRange = null,
&$columns = null
): \Closure {
return function () use ($driver, &$headers, &$data, $leftTopCorner, &$headersRange, &$dataRange, &$totalRange, &$columns) {
$ltc = preg_split('|([A-Z]+)|', $leftTopCorner, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY);
// left top corner: row
$headerRow = 1;
while ($ltc[1] > 1) {
$ltc[1]--;
$headerRow++;
yield [];
}
// left top corner: column
$firstColumn = 'A';
$indent = [];
while ($this->isColumnLE($firstColumn, $ltc[0]) && $firstColumn != $ltc[0]) {
$firstColumn = $this->strIncrement($firstColumn);
$indent[] = '';
}
// Handle with header
$lastColumn = $firstColumn;
$isFirst = true;
$hasHeaders = false;
foreach ($headers as $key => &$header) {
if ($isFirst) {
$isFirst = false;
} else {
$lastColumn = $this->strIncrement($lastColumn);
}
if ($this->hookHeader) {
// Hook: header
$header = ($this->hookHeader)($driver, $header, $key, $lastColumn, $headerRow);
}
if ($header) {
$hasHeaders = true;
}
}
unset($header);
// First iteration with headers
if ($hasHeaders) {
yield array_merge($indent, $headers);
} else {
$headerRow--;
}
// Iterations with data
$dataRow = $headerRow;
$isFirst = ! $hasHeaders;
foreach ($data as $key => $row) {
// Hook: row
if ($this->hookRow) {
$row = ($this->hookRow)($driver, $row, $key, $dataRow + 1);
}
if (is_null($row)) {
continue;
}
if ($isFirst) {
foreach ($row as $item) {
if ($isFirst) {
$isFirst = false;
} else {
$lastColumn = $this->strIncrement($lastColumn);
}
}
}
yield array_merge($indent, $row);
$dataRow++;
}
// Statistic
$headersRange = null;
if ($hasHeaders) {
$headersRange = sprintf('%s%d:%s%d', $firstColumn, $headerRow, $lastColumn, $headerRow);
}
$dataRange = null;
if ($dataRow != $headerRow) {
$dataRange = sprintf('%s%d:%s%d', $firstColumn, ($headerRow + 1), $lastColumn, $dataRow);
}
$totalRange = null;
if ($hasHeaders || $dataRow != $headerRow) {
$totalRange = sprintf(
'%s%d:%s%d',
$firstColumn,
($hasHeaders ? $headerRow : ($headerRow + 1)),
$lastColumn,
$dataRow
);
}
$columns = [];
if ($totalRange) {
$keys = array_keys($headers);
while ($this->isColumnLE($firstColumn, $lastColumn)) {
if (! $keys) {
$columns[] = $firstColumn;
} else {
$columns[array_shift($keys)] = $firstColumn;
}
$firstColumn = $this->strIncrement($firstColumn);
}
}
};
}
}
================================================
FILE: src/Mixer.php
================================================
<?php
namespace AnourValar\Office;
class Mixer
{
/**
* Mix generated documents
*
* @param \AnourValar\Office\Generated[\AnourValar\Office\Drivers\MixInterface] $generated
* @throws \LogicException
* @return \AnourValar\Office\Generated
*/
public function __invoke(...$generated): Generated
{
$referenceDriver = array_shift($generated);
if (! $referenceDriver instanceof \AnourValar\Office\Generated) {
throw new \LogicException('Input data must be instanceof Generated');
}
$referenceDriver = $referenceDriver->driver;
if (! $referenceDriver instanceof \AnourValar\Office\Drivers\MixInterface) {
throw new \LogicException('Driver must implements MixInterface.');
}
$titles = [];
$count = $referenceDriver->getSheetCount();
for ($i = 0; $i < $count; $i++) {
$referenceDriver->setSheet($i);
$titles[] = $referenceDriver->getSheetTitle();
}
foreach ($generated as $driver) {
if (! $driver instanceof \AnourValar\Office\Generated) {
throw new \LogicException('Input data must be instanceof Generated');
}
$driver = $driver->driver;
if (! $driver instanceof $referenceDriver) {
throw new \LogicException('All drivers should be instances of the same implementation.');
}
$count = $driver->getSheetCount();
for ($i = 0; $i < $count; $i++) {
$driver->setSheet($i);
$driver->setSheetTitle($titles[] = $this->getTitle($driver->getSheetTitle(), $titles));
$referenceDriver->mergeDriver($driver);
}
}
return new Generated($referenceDriver);
}
/**
* @param string $title
* @param array $titles
* @return string
*/
protected function getTitle(string $title, array $titles): string
{
while (in_array($title, $titles, true)) {
$title = preg_replace_callback(
'#\((\d+)\)$#',
fn ($patterns) => '(' . ++$patterns[1] . ')',
$title,
-1,
$count
);
if (! $count) {
$title .= ' (1)';
}
}
return $title;
}
}
================================================
FILE: src/Sheets/Parser.php
================================================
<?php
namespace AnourValar\Office\Sheets;
class Parser
{
use \AnourValar\Office\Traits\Parser;
/**
* Handle with special types of data
*
* @param mixed $data
* @return array
*/
public function canonizeData(mixed $data): array
{
if (is_object($data) && method_exists($data, 'toArray')) {
$data = $data->toArray();
}
return $data;
}
/**
* Get schema for a document
*
* @param array $values
* @param array $data
* @param array $mergeCells
* @return \AnourValar\Office\Sheets\SchemaMapper
*/
public function schema(array $values, array $data, array $mergeCells): SchemaMapper
{
$schema = new SchemaMapper();
// Step 0: Parse input arguments to a canon format
$values = $this->parseValues($values, $lastColumn);
$data = $this->parseData($data);
$mergeCells = $this->parseMergeCells($mergeCells);
// Step 1: Short path -> full path
$this->canonizeMarkers($values, $data);
// Step 2: Calculate additional rows & columns, redundant data
$dataSchema = $this->calculateDataSchema($values, $data, $mergeCells, $schema, $lastColumn);
// Step 3: Shift formulas
$this->shiftFormulas($dataSchema, $schema, $mergeCells);
// Step 4: Replace markers with data
$this->replaceMarkers($dataSchema, $data, $schema);
return $schema;
}
/**
* @param array $values
* @param mixed $lastColumn
* @return array
*/
protected function parseValues(array $values, &$lastColumn): array
{
$lastColumn = 'A';
foreach ($values as &$columns) {
$currLastColumn = array_key_last($columns);
if ($this->isColumnLE($lastColumn, $currLastColumn)) {
$lastColumn = $currLastColumn;
}
$columns = array_filter($columns, fn ($item) => $item !== null && $item !== '');
}
unset($columns);
return $values;
}
/**
* @param array $data
* @return array
*/
protected function parseData(array &$data): array
{
$result = [];
foreach ($this->dot($data) as $key => $value) {
if (is_array($value) && ! $value) {
continue;
}
$result[$key] = $value;
}
return $result;
}
/**
* @param array $mergeCells
* @return array
*/
protected function parseMergeCells(array $mergeCells): array
{
foreach ($mergeCells as &$item) {
$item = explode(':', $item);
$item[0] = preg_split('#([A-Z]+)([\d]+)#S', $item[0], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$item[1] = preg_split('#([A-Z]+)([\d]+)#S', $item[1], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
}
unset($item);
return $mergeCells;
}
/**
* @param array $values
* @param array $data
* @return void
*/
protected function canonizeMarkers(array &$values, array &$data): void
{
foreach ($values as &$columns) {
foreach ($columns as &$value) {
if (! is_string($value)) {
continue;
}
$value = preg_replace_callback(
'#\[(\$?\!\s*|\$?\=\s*)?([a-z\d\.\_\*]+)\]#iS',
function ($patterns) use ($data) {
if (array_key_exists($patterns[2], $data)) {
return $patterns[0];
}
$result = null;
foreach (explode('.', $patterns[2]) as $pattern) {
$changed = true;
$prevResult = $result;
while ($changed) {
$changed = false;
if ($this->isShortPath($result ? ($result . '.' . $pattern) : $pattern, $data)) {
if ($result) {
$result .= '.';
}
$result .= $pattern;
$changed = true;
}
if ($this->isShortPath($result . '.0', $data)) {
$result .= '.0';
$changed = true;
}
}
if ($result === $prevResult && ($pattern != '*' || mb_substr($result, -2) != '.0')) {
return $patterns[0];
}
}
if ($result && array_key_exists($result, $data) && ! is_array($data[$result])) {
$result = preg_replace('#\.0(\.|$)#S', '.*$1', $result);
return sprintf('[%s%s]', $patterns[1], $result);
}
return $patterns[0];
},
$value
);
}
unset($value);
}
unset($columns);
}
/**
* @param array $values
* @param array $data
* @param array $mergeCells
* @param \AnourValar\Office\Sheets\SchemaMapper $schema
* @param string $lastColumn
* @return array
* @psalm-suppress UnusedForeachValue
*/
protected function calculateDataSchema(
array &$values,
array &$data,
array &$mergeCells,
SchemaMapper &$schema,
string $lastColumn
): array {
$dataSchema = [];
$shift = 0;
$step = 0;
$stepLeft = 0;
$stepOrigin = 0;
$stepRows = 0;
// fill in missing rows
$prevRow = 0;
foreach (array_keys($values) as $row) {
$diff = ($row - $prevRow);
while ($diff > 1) {
$values[$row - $diff + 1] = [];
$diff--;
}
$prevRow = $row;
}
ksort($values);
foreach ($values as $row => $columns) {
$maxMergeY = 0;
$additionRows = 0;
$additionColumns = 0;
$additionColumn = null;
foreach ($columns as $column => $value) {
foreach (array_keys($data) as $markerName) {
if (! $this->hasMarker($markerName, $value)) {
continue;
}
$qty = 0;
$pattern = $markerName;
while ($pattern = $this->increment($pattern, true)) {
if (! array_key_exists($pattern, $data)) {
break;
}
$qty++;
}
$additionRows = max($additionRows, $qty);
$qty = 0;
$pattern = $markerName;
while ($pattern = $this->increment($pattern, false)) {
if (! array_key_exists($pattern, $data)) {
break;
}
$qty++;
}
if ($qty) {
$additionColumns = max($additionColumns, $qty);
$additionColumn = $column;
}
}
if (is_string($value)) {
$columns[$column] = preg_replace('#\.\*(\.|\])#S', '.0$1', $value);
}
}
if (! $stepRows && $this->shouldBeDeleted($columns, $data)) {
$this->deleteRow($schema, $mergeCells, $row + $shift);
$shift--;
continue;
}
if ($stepRows) {
$additionRows = $stepRows;
}
$currAdditionRows = $additionRows;
if (! $additionRows) {
foreach ($columns as $column => $value) {
if (
! preg_match('#\[(\$?\!\s*|\$?\=\s*)?[a-z][a-z\d\_\.]+\]#iS', (string) $value)
&& ! preg_match('#^\=[A-Z][A-Z\.\d]#', (string) $value)
) {
unset($columns[$column]);
}
}
if (! $columns) {
continue;
}
}
$curr = $additionColumn;
$additionColumnValue = isset($curr) ? ($columns[$curr] ?? null) : null;
$mergeMapX = [];
foreach ($mergeCells as $item) {
if ($additionColumn.($row + $shift) == $item[0][0].$item[0][1] && $item[0][1] == $item[1][1]) {
while ($this->isColumnLE($item[0][0], $item[1][0]) && $item[0][0] != $item[1][0]) {
$item[0][0] = $this->strIncrement($item[0][0]);
$mergeMapX[] = $item[0][0];
}
}
}
foreach ($mergeMapX as $mergeItemX) {
$curr = $this->strIncrement($curr);
}
while ($additionColumns) {
$curr = $this->strIncrement($curr);
$additionColumns--;
$additionColumnValue = $this->increments($additionColumnValue, false);
$columns[$curr] = $additionColumnValue;
$schema->copyStyle($additionColumn.($row + $shift), $curr.($row + $shift));
$schema->copyWidth($additionColumn, $curr);
if ($mergeMapX) {
$originalCurr = $curr;
foreach ($mergeMapX as $mergeItemX) {
$curr = $this->strIncrement($curr);
$schema->copyWidth($mergeItemX, $curr);
}
$schema->mergeCells(sprintf('%s%s:%s%s', $originalCurr, ($row + $shift), $curr, ($row + $shift)));
$mergeCells[] = [ [$originalCurr, ($row + $shift)], [$curr, ($row + $shift)] ]; // fill in
}
}
$dataSchema[$row + $shift] = $columns;
$originalRow = ($row + $shift);
if ($additionRows) {
$firstColumn = 'A';
while ($this->isColumnLE($firstColumn, $lastColumn)) {
if (! isset($columns[$firstColumn]) && ! $this->insideMerge($firstColumn, $originalRow, $mergeCells)) {
$columns[$firstColumn] = null;
}
$firstColumn = $this->strIncrement($firstColumn);
}
uksort($columns, fn ($a, $b) => $this->isColumnLE($a, $b) ? -1 : 1);
foreach ($columns as $currKey => $currValue) {
$hasMarker = preg_match('#\[([a-z][a-z\d\.\_]+)\]#iS', (string) $currValue);
foreach ($mergeCells as $item) {
if ($currKey.$originalRow == $item[0][0].$item[0][1] && $item[0][1] != $item[1][1]) {
if (! $hasMarker) {
unset($columns[$currKey]);
continue;
}
$maxMergeY = max($maxMergeY, ($item[1][1] - $item[0][1]));
}
}
}
$shift += $maxMergeY;
}
while ($additionRows) {
$shift += $step;
$shift++;
$additionRows--;
foreach ($columns as &$column) {
if (is_string($column)) {
$column = $this->increments($column, true);
}
}
unset($column);
$dataSchema[$row + $shift] = array_filter($columns, fn ($item) => $item !== null && $item !== '');
if (! $step) {
$this->addRow($schema, $mergeCells, $row + $shift);
}
foreach (array_keys($columns) as $curr) {
$schema->copyStyle($curr.$originalRow, $curr.($row + $shift));
foreach ($mergeCells as $item) {
if ($curr.$originalRow == $item[0][0].$item[0][1]) {
$diff = $item[1][1] - $item[0][1];
$schema->mergeCells(
sprintf('%s%s:%s%s', $item[0][0], ($row + $shift), $item[1][0], ($row + $shift + $diff))
);
}
}
}
$iterate = $maxMergeY;
while ($iterate) {
$shift++;
$this->addRow($schema, $mergeCells, $row + $shift);
$iterate--;
}
}
if ($stepLeft) {
$stepLeft--;
}
if (! $stepLeft) {
$step = 0;
$stepRows = 0;
} else {
$stepOrigin--;
$shift -= $stepOrigin - $stepLeft;
}
if ($maxMergeY) {
$stepRows = $currAdditionRows;
$step = $maxMergeY;
$stepLeft = $step;
$stepOrigin = (($maxMergeY + 1) * ($currAdditionRows)) + $step;
$shift -= $stepOrigin;
}
}
unset($values);
return $dataSchema;
}
/**
* @param array $values
* @param \AnourValar\Office\Sheets\SchemaMapper $schema
* @param array $mergeCells
* @return void
*/
protected function shiftFormulas(array &$values, SchemaMapper &$schema, array &$mergeCells): void
{
// Prepares
$ranges = [];
$map = [];
foreach ($values as $row => $columns) {
foreach ($columns as $column => $value) {
if (preg_match('#^\=[A-Z][A-Z\.\d]#', (string) $value)) {
$map[$row][$column] = $value;
}
}
}
// "Outside" shifts
foreach ($schema->getOriginal()['rows'] as $action) {
foreach ($map as $row => $columns) {
foreach ($columns as $column => $value) {
$map[$row][$column] = $values[$row][$column] = preg_replace_callback(
'#([A-Z]+)([\d]+)#S',
function ($patterns) use ($action) {
if ($action['action'] == 'add') {
if ($patterns[2] >= $action['row']) {
return $patterns[1] . ++$patterns[2];
}
} else {
if ($patterns[2] > $action['row']) {
return $patterns[1] . --$patterns[2];
}
}
return $patterns[0];
},
$value
);
}
}
}
// "Inside" shifts
$prev = 0;
$prevAction = null;
foreach ($schema->getOriginal()['rows'] as $action) {
if ($prevAction && $prevAction['row'] + 1 == $action['row'] && $action['action'] == 'add' && $prevAction['action'] == 'add') {
$prev++;
} else {
if ($prevAction && $prevAction['action'] == 'add') {
$ranges[] = ['from' => ($prevAction['row'] - $prev - 1), 'to' => ($prevAction['row'])];
}
$prev = 0;
}
foreach ($map as $row => $columns) {
foreach ($columns as $column => $value) {
$map[$row][$column] = $values[$row][$column] = preg_replace_callback(
'#([A-Z]+)([\d]+)#S',
function ($patterns) use ($action, $row, $prev) {
if ($action['action'] == 'add') {
if ($action['row'] == $row && ($row - $prev) == ($patterns[2] + 1)) {
return $patterns[1] . (++$patterns[2] + $prev);
}
}
return $patterns[0];
},
$value
);
}
}
$prevAction = $action;
}
if ($prev || ($prevAction && $prevAction['action'] == 'add')) {
$ranges[] = ['from' => ($prevAction['row'] - $prev - 1), 'to' => ($prevAction['row'])];
}
// Dynamic table ranges
foreach ($ranges as $key => $value) {
foreach ($mergeCells as $merge) {
if (
$value['from'] >= $merge[0][1]
&& $value['from'] <= $merge[1][1]
&& $merge[0][1] != $merge[1][1]
&& preg_match('#\[([a-z][a-z\d\.\_]+)\]#iS', ($values[$merge[0][1]][$merge[0][0]] ?? ''))
) {
unset($ranges[$key]);
}
}
}
foreach ($map as $row => $columns) {
foreach ($columns as $column => $value) {
$map[$row][$column] = $values[$row][$column] = preg_replace_callback(
'#([A-Z]+)([\d]+)\:([A-Z]+)([\d]+)#S',
function ($patterns) use ($ranges) {
if ($patterns[2] == $patterns[4]) {
foreach ($ranges as $range) {
if ($patterns[2] == $range['from']) {
return sprintf('%s%d:%s%d', $patterns[1], $patterns[2], $patterns[3], $range['to']);
}
}
}
return $patterns[0];
},
$value
);
}
}
}
/**
* @param array $dataSchema
* @param array $data
* @param \AnourValar\Office\Sheets\SchemaMapper $schema
* @return void
*/
protected function replaceMarkers(array &$dataSchema, array &$data, SchemaMapper &$schema): void
{
$canonizeKeys = ['scalar' => [], 'closure' => []];
$canonizeValues = ['scalar' => [], 'closure' => []];
foreach ($data as $from => $to) {
if (! preg_match('#^[a-z][a-z\d\.\_]+$#iS', $from)) {
continue;
}
if (is_scalar($to) || is_null($to)) {
$canonizeKeys['scalar'][] = "[$from]";
$canonizeValues['scalar'][] = $to;
} else {
$canonizeKeys['closure'][] = "[$from]";
$canonizeValues['closure'][] = $to;
}
}
foreach ($dataSchema as $row => $columns) {
ksort($columns);
foreach ($columns as $column => $value) {
if (is_string($value) && $this->shouldBeDeleted([$value], $data, '$')) {
$value = null;
}
if (is_string($value) && mb_strlen($value)) {
$value = preg_replace('#\[(\$?\!\s*|\$?\=\s*)[a-z][a-z\d\_\.]+\]#iS', '', $value);
$value = trim($value);
if (($key = array_search($value, $canonizeKeys['scalar'])) !== false) { // type (cast) support
$value = $canonizeValues['scalar'][$key];
} elseif (($key = array_search($value, $canonizeKeys['closure'])) !== false) {
$value = $canonizeValues['closure'][$key];
} else {
$value = str_replace($canonizeKeys['scalar'], $canonizeValues['scalar'], $value);
}
}
if (is_string($value) && mb_strlen($value)) {
$value = preg_replace('#\[[a-z][a-z\d\_\.]+\]#iS', '', $value);
$value = trim($value);
}
if (is_string($value) && ! mb_strlen($value)) {
$value = null;
}
$schema->addData($row, $column, $value);
}
}
}
/**
* @param string $path
* @param array $markers
* @return bool
*/
private function isShortPath(string $path, array $markers): bool
{
if (array_key_exists($path, $markers)) {
return true;
}
if (! str_ends_with($path, '.')) {
$path .= '.';
}
foreach (array_keys($markers) as $marker) {
if (strpos($marker, $path) === 0) {
return true;
}
}
return false;
}
/**
* @param string $marker
* @param string $value
* @return bool
*/
private function hasMarker(string $marker, ?string $value): bool
{
$value = preg_replace('#\.\*(\.|$|)#S', '.0$1', (string) $value, -1, $count);
if (! $count) {
return false;
}
if (strpos((string) $value, "[$marker]") !== false) {
return true;
}
return false;
}
/**
* @param array $columns
* @param array $data
* @param string $prefix
* @return bool
*/
private function shouldBeDeleted(array $columns, array &$data, string $prefix = ''): bool
{
$prefix = preg_quote($prefix);
foreach ($columns as $column) {
if (is_null($column)) {
continue;
}
preg_match_all("#\[{$prefix}\=\s*([a-z\d\.\_]+)\]#i", $column, $patterns);
foreach (($patterns[1] ?? []) as $marker) {
if (! empty($data[$marker])) {
continue;
}
foreach ($data as $key => $value) {
if (strpos($key, $marker.'.') === 0 && ! empty($value)) {
continue 2;
}
}
return true;
}
preg_match_all("#\[{$prefix}\!\s*([a-z\d\.\_]+)\]#i", $column, $patterns);
foreach (($patterns[1] ?? []) as $marker) {
if (! empty($data[$marker])) {
return true;
}
foreach ($data as $key => $value) {
if (strpos($key, $marker.'.') === 0 && ! empty($value)) {
return true;
}
}
}
}
return false;
}
/**
* @param string $markerName
* @param bool $first
* @param int $shift
* @return string|null
*/
private function increment(string $markerName, bool $first, int $shift = 1): ?string
{
$markerName = explode('.', $markerName);
if (! $first) {
$markerName = array_reverse($markerName);
}
if (! $first) {
$qty = 0;
foreach ($markerName as $item) {
if (is_numeric($item)) {
$qty++;
}
}
if ($qty < 2) {
return null;
}
}
foreach ($markerName as &$item) {
if (is_numeric($item)) {
$item += $shift;
if (! $first) {
$markerName = array_reverse($markerName);
}
return implode('.', $markerName);
}
}
unset($item);
return null;
}
/**
* @param string $value
* @param bool $first
* @param int $shift
* @return string|null
*/
private function increments(string $value, bool $first, int $shift = 1): ?string
{
return preg_replace_callback(
'#\[(\$?\!\s*|\$?\=\s*)?([a-z][a-z\d\.\_]+)\]#iS',
function ($patterns) use ($first, $shift) {
$patterns[2] = $this->increment($patterns[2], $first, $shift);
if ($patterns[2]) {
return '[' . $patterns[1] . $patterns[2] . ']';
}
return $patterns[0];
},
$value
);
}
/**
* @param \AnourValar\Office\Sheets\SchemaMapper $schema
* @param array $mergeCells
* @param int $row
* @return void
*/
private function addRow(SchemaMapper &$schema, array &$mergeCells, int $row): void
{
$schema->addRow($row);
foreach ($mergeCells as &$mergeCell) {
if ($mergeCell[0][1] >= $row) {
$mergeCell[0][1]++;
}
if ($mergeCell[1][1] >= $row) {
$mergeCell[1][1]++;
}
}
unset($mergeCell);
}
/**
* @param \AnourValar\Office\Sheets\SchemaMapper $schema
* @param array $mergeCells
* @param int $row
* @return void
*/
private function deleteRow(SchemaMapper &$schema, array &$mergeCells, int $row): void
{
$schema->deleteRow($row);
foreach ($mergeCells as &$mergeCell) {
if ($mergeCell[0][1] >= $row) {
$mergeCell[0][1]--;
}
if ($mergeCell[1][1] >= $row) {
$mergeCell[1][1]--;
}
}
unset($mergeCell);
}
/**
* @param string $column
* @param int $row
* @param array $mergeCells
* @return bool
*/
private function insideMerge(string $column, int $row, array &$mergeCells): bool
{
foreach ($mergeCells as $item) {
if (
$this->isColumnLE($item[0][0], $column)
&& $this->isColumnGE($item[1][0], $column)
&& $item[0][1] <= $row
&& $item[1][1] >= $row
&& ($item[0][0] != $column || $item[0][1] != $row)
) {
return true;
}
}
return false;
}
}
================================================
FILE: src/Sheets/SchemaMapper.php
================================================
<?php
namespace AnourValar\Office\Sheets;
class SchemaMapper
{
/**
* @var array
*/
protected array $payload = [
'data' => [], //[ 1 => ['A' => 'foo'], '2' => ['B' => 'bar'] ]
'rows' => [], //[ ['action' => 'add', 'row' => 1, 'qty' => 1], ['action' => 'delete', 'row' => 2, 'qty' => 1] ]
'copy_style' => [], //[ ['from' => 'A1', 'to' => 'A2'] ]
'merge_cells' => [], //[ 'A1:B1', 'C1:D1']
'copy_width' => [], //[ ['from' => 'B', 'to' => 'C'] ]
];
/**
* @return array
*/
public function toArray(): array
{
ksort($this->payload['data']);
$this->normalizeRows($this->payload['rows']);
$this->normalizeCells($this->payload['copy_style']);
sort($this->payload['copy_width']);
return $this->payload;
}
/**
* @return array
*/
public function getOriginal(): array
{
return $this->payload;
}
/**
* @param int $row
* @param string $column
* @param mixed $value
* @return self
*/
public function addData(int $row, string $column, mixed $value): self
{
$this->payload['data'][$row][$column] = $value;
return $this;
}
/**
* @param int $rowBefore
* @return self
*/
public function addRow(int $rowBefore): self
{
$this->payload['rows'][] = ['action' => 'add', 'row' => $rowBefore, 'qty' => 1];
return $this;
}
/**
* @param int $row
* @return self
*/
public function deleteRow(int $row): self
{
$this->payload['rows'][] = ['action' => 'delete', 'row' => $row, 'qty' => 1];
return $this;
}
/**
* @param string $from
* @param string $to
* @return self
*/
public function copyStyle(string $from, string $to): self
{
$this->payload['copy_style'][$from.$to] = ['from' => $from, 'to' => $to];
return $this;
}
/**
* @param string $ceilRange
* @return self
*/
public function mergeCells(string $ceilRange): self
{
$this->payload['merge_cells'][] = $ceilRange;
return $this;
}
/**
* @param string $from
* @param string $to
* @return self
*/
public function copyWidth(string $from, string $to): self
{
$this->payload['copy_width'][$from . $to] = ['from' => $from, 'to' => $to];
return $this;
}
/**
* @param array $rows
* @return void
*/
protected function normalizeRows(array &$rows): void
{
$optimizedRows = [];
$curr = [];
foreach ($rows as $item) {
if ($curr) {
if ($item['action'] == 'add' && $curr['action'] == $item['action'] && $item['row'] == ($curr['row'] + $curr['qty'])) {
$curr['qty']++;
} else {
$optimizedRows[] = ['action' => $curr['action'], 'row' => $curr['row'], 'qty' => $curr['qty']];
$curr = null;
}
}
if (! $curr) {
$curr = ['action' => $item['action'], 'row' => $item['row'], 'qty' => $item['qty']];
}
}
if ($curr) {
$optimizedRows[] = ['action' => $curr['action'], 'row' => $curr['row'], 'qty' => $curr['qty']];
}
$rows = $optimizedRows;
}
/**
* @param array $data
* @return void
*/
protected function normalizeCells(array &$data): void
{
ksort($data, SORT_NATURAL);
$data = array_values($data);
$optimizedData = [];
$curr = [];
foreach ($data as $item) {
$expect = preg_split('#([A-Z]+)([\d]+)#S', $item['to'], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
$expect[1]++; // @phpstan-ignore-line
$expect = implode('', $expect);
if ($curr) {
if ($item['from'] == $curr['from'] && $item['to'] == $curr['expect']) {
$curr['append'] = ':' . $curr['expect'];
$curr['expect'] = $expect;
} else {
$optimizedData[] = ['from' => $curr['from'], 'to' => $curr['to'] . $curr['append']];
$curr = null;
}
}
if (! $curr) {
$curr = ['from' => $item['from'], 'to' => $item['to'], 'append' => '', 'expect' => $expect];
}
}
if ($curr) {
$optimizedData[] = ['from' => $curr['from'], 'to' => $curr['to'] . $curr['append']];
}
$data = $optimizedData;
}
}
================================================
FILE: src/SheetsService.php
================================================
<?php
namespace AnourValar\Office;
use AnourValar\Office\Drivers\SheetsInterface;
class SheetsService
{
/**
* @var \AnourValar\Office\Drivers\SheetsInterface
*/
protected \AnourValar\Office\Drivers\SheetsInterface $driver;
/**
* @var \AnourValar\Office\Sheets\Parser
*/
protected \AnourValar\Office\Sheets\Parser $parser;
/**
* Handle template's loading
*
* @var \Closure(SheetsInterface $driver, string $templateFile, Format $templateFormat): SheetsInterface
*/
protected ?\Closure $hookLoad = null;
/**
* Actions with template before data inserted
*
* @var \Closure(SheetsInterface $driver, array &$data): void
*/
protected ?\Closure $hookBefore = null;
/**
* Cell's value handler (on set)
*
* @var \Closure(SheetsInterface $driver, string $column, int $row, mixed $value, int $sheetIndex): mixed
*/
protected ?\Closure $hookValue = null;
/**
* Actions with template after data inserted
*
* @var \Closure(SheetsInterface $driver): void
*/
protected ?\Closure $hookAfter = null;
/**
* @param \AnourValar\Office\Drivers\SheetsInterface $driver
* @param \AnourValar\Office\Sheets\Parser $parser
* @return void
*/
public function __construct(
SheetsInterface $driver = new \AnourValar\Office\Drivers\PhpSpreadsheetDriver(),
\AnourValar\Office\Sheets\Parser $parser = new \AnourValar\Office\Sheets\Parser()
) {
$this->driver = $driver;
$this->parser = $parser;
}
/**
* Generate a document from the template (sheets)
*
* @param string|\Stringable $templateFile
* @param mixed $data
* @param bool $autoCellFormat
* @return \AnourValar\Office\Generated
*/
public function generate(string|\Stringable $templateFile, mixed $data, bool $autoCellFormat = false): Generated
{
// Handle with input data
$data = $this->parser->canonizeData($data);
// Open the template
$templateFormat = Format::tryFrom(mb_strtolower(pathinfo($templateFile, PATHINFO_EXTENSION))) ?? Format::Xlsx;
if ($this->hookLoad) {
$driver = ($this->hookLoad)($this->driver, $templateFile, $templateFormat);
if ($driver instanceof Generated) {
$driver = $driver->driver;
}
} else {
$driver = $this->driver->load($templateFile, $templateFormat);
}
// Hook: before
if ($this->hookBefore) {
($this->hookBefore)($driver, $data);
}
// Handle sheets
$count = $driver->getSheetCount();
for ($sheetIndex = 0; $sheetIndex < $count; $sheetIndex++) {
$driver->setSheet($sheetIndex);
$this->handleSheet($driver, $data, $sheetIndex, $autoCellFormat);
}
// Hook: after
if ($this->hookAfter) {
($this->hookAfter)($driver);
}
// Return
return new Generated($driver);
}
/**
* Set hookLoad
*
* @param ?\Closure $closure
* @return self
*/
public function hookLoad(?\Closure $closure): self
{
$this->hookLoad = $closure;
return $this;
}
/**
* Set hookBefore
*
* @param ?\Closure $closure
* @return self
*/
public function hookBefore(?\Closure $closure): self
{
$this->hookBefore = $closure;
return $this;
}
/**
* Set hookValue
*
* @param ?\Closure $closure
* @return self
*/
public function hookValue(?\Closure $closure): self
{
$this->hookValue = $closure;
return $this;
}
/**
* Set hookAfter
*
* @param ?\Closure $closure
* @return self
*/
public function hookAfter(?\Closure $closure): self
{
$this->hookAfter = $closure;
return $this;
}
/**
* @param \AnourValar\Office\Drivers\SheetsInterface $driver
* @param array $data
* @param int $sheetIndex
* @throws \LogicException
* @return void
*/
protected function handleSheet(SheetsInterface &$driver, array &$data, int $sheetIndex, bool $autoCellFormat): void
{
// Get schema of the document
$schema = $this->parser->schema($driver->getValues(null), $data, $driver->getMergeCells())->toArray();
// Rows
foreach ($schema['rows'] as $row) {
if ($row['action'] == 'add') {
$driver->addRow($row['row'], $row['qty']);
} elseif ($row['action'] == 'delete') {
$driver->deleteRow($row['row'], $row['qty']);
} else {
throw new \LogicException('Incorrect usage.');
}
}
// Copy style & cell format
foreach ($schema['copy_style'] as $item) {
$driver->copyStyle($item['from'], $item['to']);
if (! $autoCellFormat) {
$driver->copyCellFormat($item['from'], $item['to']);
}
}
// Merge cells
foreach ($schema['merge_cells'] as $item) {
$driver->mergeCells($item);
}
// Copy width
foreach ($schema['copy_width'] as $item) {
$driver->copyWidth($item['from'], $item['to']);
}
// Data
$driver->setValues($this->handleData($schema['data'], $driver, $sheetIndex), $autoCellFormat);
}
/**
* @param array $data
* @param \AnourValar\Office\Drivers\SheetsInterface $driver
* @param int $sheetIndex
* @return array
*/
protected function handleData(array $data, SheetsInterface $driver, int $sheetIndex): array
{
foreach ($data as $row => &$columns) {
foreach ($columns as $column => &$value) {
if ($value instanceof \Closure) {
// Private Closure
$value = $value($driver, $column, $row);
if (is_null($value)) {
unset($data[$row][$column]);
}
} elseif ($this->hookValue) {
// Hook: value
$value = ($this->hookValue)($driver, $column, $row, $value, $sheetIndex);
if (is_null($value)) {
unset($data[$row][$column]);
}
}
}
unset($value);
}
unset($columns);
return $data;
}
}
================================================
FILE: src/Traits/Parser.php
================================================
<?php
namespace AnourValar\Office\Traits;
trait Parser
{
/**
* @param array $data
* @param string $prefix
* @return array
*/
protected function dot(array $data, string $prefix = ''): array
{
$result = [];
foreach ($data as $key => $value) {
if (is_array($value)) {
$result = array_replace($result, $this->dot($value, $prefix.$key.'.'));
} else {
$result[$prefix.$key] = $value;
}
}
return $result;
}
/**
* @param string $compareColumn
* @param string $referenceColumn
* @return bool
*/
protected function isColumnLE(string $compareColumn, string $referenceColumn): bool
{
$compareLength = strlen($compareColumn);
$referenceLength = strlen($referenceColumn);
if ($compareLength < $referenceLength) {
return true;
}
if ($compareLength > $referenceLength) {
return false;
}
return $compareColumn <= $referenceColumn;
}
/**
* @param string $compareColumn
* @param string $referenceColumn
* @return bool
*/
protected function isColumnGE(string $compareColumn, string $referenceColumn): bool
{
$compareLength = strlen($compareColumn);
$referenceLength = strlen($referenceColumn);
if ($compareLength > $referenceLength) {
return true;
}
if ($compareLength < $referenceLength) {
return false;
}
return $compareColumn >= $referenceColumn;
}
/**
* Polyfill
*
* @param string $value
* @return string
*/
protected function strIncrement(string $value): string
{
if (PHP_VERSION_ID >= 80300) {
return str_increment($value);
}
$value++; // @phpstan-ignore-line
return $value;
}
}
================================================
FILE: src/Traits/XFormat.php
================================================
<?php
namespace AnourValar\Office\Traits;
trait XFormat
{
/**
* @param \DateTimeInterface $date
* @return float
*/
protected function excelDate(\DateTimeInterface $date): float
{
$year = (int) $date->format('Y');
$month = (int) $date->format('m');
$day = (int) $date->format('d');
$hours = (int) $date->format('H');
$minutes = (int) $date->format('i');
$seconds = (int) $date->format('s');
$leapYear = true;
if ($year == 1900 && $month <= 2) {
$leapYear = false;
}
$baseDate = 2415020;
if ($month > 2) {
$month -= 3;
} else {
$month += 9;
--$year;
}
$century = (int) substr($year, 0, 2);
$decade = (int) substr($year, 2, 2);
$excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5);
$excelDate += $day + 1721119 - $baseDate + $leapYear;
$excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400;
return (float) $excelDate + $excelTime;
}
/**
* @param string|null $value
* @return string
*/
protected function escape(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8', true);
}
}
================================================
FILE: tests/GridServiceTest.php
================================================
<?php
namespace AnourValar\Office\Tests;
class GridServiceTest extends \PHPUnit\Framework\TestCase
{
/**
* @return void
*/
public function test_generate_statistic_with_headers()
{
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:A1', $headersRange);
$this->assertSame(null, $dataRange);
$this->assertSame('A1:A1', $totalRange);
$this->assertSame(['A'], $columns);
})
->generate(
['foo'],
[ ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:A1', $headersRange);
$this->assertSame('A2:A2', $dataRange);
$this->assertSame('A1:A2', $totalRange);
$this->assertSame(['A'], $columns);
})
->generate(
['foo'],
[ ['111'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:A1', $headersRange);
$this->assertSame('A2:A3', $dataRange);
$this->assertSame('A1:A3', $totalRange);
$this->assertSame(['A'], $columns);
})
->generate(
['foo'],
[ ['foo-1'], ['foo-2'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:A1', $headersRange);
$this->assertSame('A2:A4', $dataRange);
$this->assertSame('A1:A4', $totalRange);
$this->assertSame(['A'], $columns);
})
->generate(
['foo'],
[ ['foo-1'], ['foo-2'], ['foo-3'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:B1', $headersRange);
$this->assertSame(null, $dataRange);
$this->assertSame('A1:B1', $totalRange);
$this->assertSame(['A', 'B'], $columns);
})
->generate(
['foo', 'bar'],
[ ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:B1', $headersRange);
$this->assertSame('A2:B2', $dataRange);
$this->assertSame('A1:B2', $totalRange);
$this->assertSame(['A', 'B'], $columns);
})
->generate(
['foo', 'bar'],
[ ['foo-1', 'bar-1'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:B1', $headersRange);
$this->assertSame('A2:B3', $dataRange);
$this->assertSame('A1:B3', $totalRange);
$this->assertSame(['A', 'B'], $columns);
})
->generate(
['foo', 'bar'],
[ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:B1', $headersRange);
$this->assertSame('A2:B4', $dataRange);
$this->assertSame('A1:B4', $totalRange);
$this->assertSame(['A', 'B'], $columns);
})
->generate(
['foo', 'bar'],
[ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'], ['foo-3', 'bar-3'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:C1', $headersRange);
$this->assertSame(null, $dataRange);
$this->assertSame('A1:C1', $totalRange);
$this->assertSame(['A', 'B', 'C'], $columns);
})
->generate(
['foo', 'bar', 'baz'],
[ ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:C1', $headersRange);
$this->assertSame('A2:C2', $dataRange);
$this->assertSame('A1:C2', $totalRange);
$this->assertSame(['A', 'B', 'C'], $columns);
})
->generate(
['foo', 'bar', 'baz'],
[ ['foo-1', 'bar-1', 'baz-1'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:C1', $headersRange);
$this->assertSame('A2:C3', $dataRange);
$this->assertSame('A1:C3', $totalRange);
$this->assertSame(['A', 'B', 'C'], $columns);
})
->generate(
['foo', 'bar', 'baz'],
[ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('A1:C1', $headersRange);
$this->assertSame('A2:C4', $dataRange);
$this->assertSame('A1:C4', $totalRange);
$this->assertSame(['A', 'B', 'C'], $columns);
})
->generate(
['foo', 'bar', 'baz'],
[ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'], ['foo-3', 'bar-3', 'baz-3'] ],
);
}
/**
* @return void
*/
public function test_generate_statistic_with_headers_with_shift()
{
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:C5', $headersRange);
$this->assertSame(null, $dataRange);
$this->assertSame('C5:C5', $totalRange);
$this->assertSame(['one' => 'C'], $columns);
})
->generate(
['one' => 'foo'],
[ ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:C5', $headersRange);
$this->assertSame('C6:C6', $dataRange);
$this->assertSame('C5:C6', $totalRange);
$this->assertSame(['C'], $columns);
})
->generate(
['foo'],
[ ['111'] ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:C5', $headersRange);
$this->assertSame('C6:C7', $dataRange);
$this->assertSame('C5:C7', $totalRange);
$this->assertSame(['C'], $columns);
})
->generate(
['foo'],
[ ['foo-1'], ['foo-2'] ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:C5', $headersRange);
$this->assertSame('C6:C8', $dataRange);
$this->assertSame('C5:C8', $totalRange);
$this->assertSame(['C'], $columns);
})
->generate(
['foo'],
[ ['foo-1'], ['foo-2'], ['foo-3'] ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:D5', $headersRange);
$this->assertSame(null, $dataRange);
$this->assertSame('C5:D5', $totalRange);
$this->assertSame(['C', 'D'], $columns);
})
->generate(
['foo', 'bar'],
[ ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:D5', $headersRange);
$this->assertSame('C6:D6', $dataRange);
$this->assertSame('C5:D6', $totalRange);
$this->assertSame(['C', 'D'], $columns);
})
->generate(
['foo', 'bar'],
[ ['foo-1', 'bar-1'] ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:D5', $headersRange);
$this->assertSame('C6:D7', $dataRange);
$this->assertSame('C5:D7', $totalRange);
$this->assertSame(['C', 'D'], $columns);
})
->generate(
['foo', 'bar'],
[ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'] ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:D5', $headersRange);
$this->assertSame('C6:D8', $dataRange);
$this->assertSame('C5:D8', $totalRange);
$this->assertSame(['C', 'D'], $columns);
})
->generate(
['foo', 'bar'],
[ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'], ['foo-3', 'bar-3'] ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:E5', $headersRange);
$this->assertSame(null, $dataRange);
$this->assertSame('C5:E5', $totalRange);
$this->assertSame(['C', 'D', 'E'], $columns);
})
->generate(
['foo', 'bar', 'baz'],
[ ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:E5', $headersRange);
$this->assertSame('C6:E6', $dataRange);
$this->assertSame('C5:E6', $totalRange);
$this->assertSame(['C', 'D', 'E'], $columns);
})
->generate(
['foo', 'bar', 'baz'],
[ ['foo-1', 'bar-1', 'baz-1'] ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:E5', $headersRange);
$this->assertSame('C6:E7', $dataRange);
$this->assertSame('C5:E7', $totalRange);
$this->assertSame(['C', 'D', 'E'], $columns);
})
->generate(
['foo', 'bar', 'baz'],
[ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'] ],
'C5'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame('C5:E5', $headersRange);
$this->assertSame('C6:E8', $dataRange);
$this->assertSame('C5:E8', $totalRange);
$this->assertSame(['one' => 'C', 'two' => 'D', 'three' => 'E'], $columns);
})
->generate(
['one' => 'foo', 'two' => 'bar', 'three' => 'baz'],
[ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'], ['foo-3', 'bar-3', 'baz-3'] ],
'C5'
);
}
/**
* @return void
*/
public function test_generate_statistic_without_headers()
{
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame(null, $dataRange);
$this->assertSame(null, $totalRange);
$this->assertSame([], $columns);
})
->generate(
[],
[ ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:A1', $dataRange);
$this->assertSame('A1:A1', $totalRange);
$this->assertSame(['A'], $columns);
})
->generate(
[],
[ ['111'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:A2', $dataRange);
$this->assertSame('A1:A2', $totalRange);
$this->assertSame(['A'], $columns);
})
->generate(
[],
[ ['foo-1'], ['foo-2'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:A3', $dataRange);
$this->assertSame('A1:A3', $totalRange);
$this->assertSame(['A'], $columns);
})
->generate(
[],
[ ['foo-1'], ['foo-2'], ['foo-3'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:B1', $dataRange);
$this->assertSame('A1:B1', $totalRange);
$this->assertSame(['A', 'B'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:B2', $dataRange);
$this->assertSame('A1:B2', $totalRange);
$this->assertSame(['A', 'B'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:B3', $dataRange);
$this->assertSame('A1:B3', $totalRange);
$this->assertSame(['A', 'B'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'], ['foo-3', 'bar-3'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:C1', $dataRange);
$this->assertSame('A1:C1', $totalRange);
$this->assertSame(['A', 'B', 'C'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1', 'baz-1'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:C2', $dataRange);
$this->assertSame('A1:C2', $totalRange);
$this->assertSame(['A', 'B', 'C'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'] ],
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('A1:C3', $dataRange);
$this->assertSame('A1:C3', $totalRange);
$this->assertSame(['A', 'B', 'C'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'], ['foo-3', 'bar-3', 'baz-3'] ],
);
}
/**
* @return void
*/
public function test_generate_statistic_without_headers_with_shift()
{
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame(null, $dataRange);
$this->assertSame(null, $totalRange);
$this->assertSame([], $columns);
})
->generate(
[],
[ ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:D4', $dataRange);
$this->assertSame('D4:D4', $totalRange);
$this->assertSame(['D'], $columns);
})
->generate(
[],
[ ['111'] ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:D5', $dataRange);
$this->assertSame('D4:D5', $totalRange);
$this->assertSame(['D'], $columns);
})
->generate(
[],
[ ['foo-1'], ['foo-2'] ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:D6', $dataRange);
$this->assertSame('D4:D6', $totalRange);
$this->assertSame(['D'], $columns);
})
->generate(
[],
[ ['foo-1'], ['foo-2'], ['foo-3'] ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:E4', $dataRange);
$this->assertSame('D4:E4', $totalRange);
$this->assertSame(['D', 'E'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1'] ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:E5', $dataRange);
$this->assertSame('D4:E5', $totalRange);
$this->assertSame(['D', 'E'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'] ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:E6', $dataRange);
$this->assertSame('D4:E6', $totalRange);
$this->assertSame(['D', 'E'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1'], ['foo-2', 'bar-2'], ['foo-3', 'bar-3'] ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:F4', $dataRange);
$this->assertSame('D4:F4', $totalRange);
$this->assertSame(['D', 'E', 'F'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1', 'baz-1'] ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:F5', $dataRange);
$this->assertSame('D4:F5', $totalRange);
$this->assertSame(['D', 'E', 'F'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'] ],
'D4'
);
(new \AnourValar\Office\GridService($this->getDriver()))
->hookAfter(function ($driver, ?string $headersRange, ?string $dataRange, ?string $totalRange, array $columns) {
$this->assertSame(null, $headersRange);
$this->assertSame('D4:F6', $dataRange);
$this->assertSame('D4:F6', $totalRange);
$this->assertSame(['D', 'E', 'F'], $columns);
})
->generate(
[],
[ ['foo-1', 'bar-1', 'baz-1'], ['foo-2', 'bar-2', 'baz-2'], ['foo-3', 'bar-3', 'baz-3'] ],
'D4'
);
}
/**
* @return \AnourValar\Office\Drivers\GridInterface
* @psalm-suppress UnusedForeachValue
*/
protected function getDriver(): \AnourValar\Office\Drivers\GridInterface
{
return new class () implements \AnourValar\Office\Drivers\GridInterface {
public function create(): self
{
return $this;
}
public function setGrid(iterable $data): self
{
foreach ($data as $item) {
}
return $this;
}
public function save(string $file, \AnourValar\Office\Format $format): void
{
}
};
}
}
================================================
FILE: tests/SheetsParserTest.php
================================================
<?php
namespace AnourValar\Office\Tests;
class SheetsParserTest extends \PHPUnit\Framework\TestCase
{
/**
* @var \AnourValar\Office\Sheets\Parser
*/
protected \AnourValar\Office\Sheets\Parser $service;
/**
* @see \PHPUnit\Framework\TestCase
*
* @return void
*/
protected function setUp(): void
{
$this->service = new \AnourValar\Office\Sheets\Parser();
}
/**
* @return void
*/
public function test_collision_names()
{
$data = [
[
'values' => [
1 => [
'A' => '[title]',
],
2 => [
'A' => '[request.title]',
],
3 => [
'A' => '[response.body]',
],
4 => [
'A' => '[body]',
],
5 => [
'A' => '[products.title.title]',
],
],
'data' => [
'title' => 'foo',
'request' => [],
'response' => ['body' => 'bar'],
'body' => [],
'products' => [
'title' => [111],
],
],
],
[
'values' => [
1 => [
'A' => '[title]',
],
2 => [
'A' => '[request.title]',
],
3 => [
'A' => '[response.body]',
],
4 => [
'A' => '[body]',
],
5 => [
'A' => '[products.title.title]',
],
],
'data' => [
'title' => 'foo',
'request' => null,
'response' => ['body' => 'bar'],
'body' => null,
'products' => [
'title' => [111],
],
],
],
[
'values' => [
1 => [
'A' => '[title]',
],
2 => [
'A' => '[request.title]',
],
3 => [
'A' => '[response.body]',
],
4 => [
'A' => '[body]',
],
5 => [
'A' => '[products.title.title]',
],
],
'data' => [
'title' => 'foo',
'response' => ['body' => 'bar'],
'products' => [
'title' => [111],
],
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => ['A' => 'foo'],
2 => ['A' => null],
3 => ['A' => 'bar'],
4 => ['A' => null],
5 => ['A' => null],
],
'rows' => [],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"$id"
);
}
}
/**
* @return void
*/
public function test_empty()
{
$data = [
[
'values' => [
1 => [
'A' => '[foo] []',
'B' => '[]',
'C' => '[bar] []',
'D' => '[foo] [baz.]',
'E' => '[foo] [б]',
'F' => '[foo] [$]',
'G' => '[foo] [%]',
'H' => '[foo] [5]',
'K' => '[foo] [a]',
],
],
'data' => [
'' => 'NO',
'bar' => 'BAR',
'baz' => ['' => 'BAZ'],
'5' => 'NO',
'б' => 'NO',
'$' => 'NO',
'%' => 'NO',
'a' => 'NO',
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'A' => '[]',
'C' => 'BAR []',
'D' => 'BAZ',
'E' => '[б]',
'F' => '[$]',
'G' => '[%]',
'H' => '[5]',
'K' => '[a]',
],
],
'rows' => [],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"$id"
);
}
}
/**
* @return void
*/
public function test_schema_scalar()
{
$data = [
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'C' => 'test [foo] test',
'E' => '[test10]',
'F' => '[test]',
'G' => '[test1]',
'H' => 'hello [$=foo]',
'I' => '[$= foo] world',
'J' => '[$! foo] test',
'K' => '[$! foo] test',
'L' => '[$= foo1] test',
'Q' => '=A1+B2+C3+D4+E5',
'X' => '= A1',
'W' => '=1',
'Y' => '=AB100 + [test]',
],
2 => [
'A' => '[bar]',
'B' => 'test [bar] test',
'C' => 'bar',
'D' => '[a.b] -> [a.c] -> [a.d] -> [a.e.f]',
'H' => '[hello]',
'J' => '[world]',
'K' => '[k] [9] [9k] [k9]',
'Y' => null,
],
'3' => [
'D' => '[bar] [!bar]',
'Y' => null,
],
'4' => [
'D' => 'hello world',
'Y' => null,
],
'5' => [
'A' => 1,
'B' => 2,
'Y' => null,
],
'6' => [
'Q' => '=A1+B2+C3+D4+E5',
'Y' => null,
],
],
'data' => [
'foo' => 'hello',
'bar' => 'world',
'a' => ['b' => '11', 'c' => '22', 'e' => ['f' => 'oops']],
'test10' => 12.5,
'test' => '3',
'test1' => 5,
'hello' => null,
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'B' => 'hello',
'C' => 'test hello test',
'E' => 12.5,
'F' => '3',
'G' => 5,
'H' => 'hello',
'I' => 'world',
'J' => null,
'K' => null,
'L' => null,
'Q' => '=A1+B2+C3+D3+E4',
'Y' => '=AB99 + 3',
],
2 => [
'A' => 'world',
'B' => 'test world test',
'D' => '11 -> 22 -> -> oops',
'H' => null,
'J' => null,
'K' => '[k] [9] [9k]',
],
5 => [
'Q' => '=A1+B2+C3+D3+E4',
],
],
'rows' => [['action' => 'delete', 'row' => 3, 'qty' => 1]],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"$id"
);
}
}
/**
* @return void
*/
public function test_schema_not_scalar()
{
$data = [
[
'values' => [
1 => [
'A' => '[foo]',
'B' => '[baz]',
],
2 => [
'A' => 'hello [bar] world',
'B' => null,
],
3 => [
'A' => '[test] [=foo]',
'B' => null,
],
4 => [
'A' => '[test2] [=bar]',
'B' => null,
],
5 => [
'A' => '[test2] [!foo]',
'B' => null,
],
],
'data' => [
'foo' => function () {
},
'baz' => new \DateTime('2022-11-16'),
'test' => function () {
},
'test2' => function () {
throw new \LogicException('oops');
},
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'A' => $item['data']['foo'],
'B' => $item['data']['baz'],
],
2 => [
'A' => 'hello world',
],
3 => [
'A' => $item['data']['test'],
],
],
'rows' => [
['action' => 'delete', 'row' => 4, 'qty' => 1],
['action' => 'delete', 'row' => 4, 'qty' => 1],
],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"$id"
);
}
$this->assertSame(
[
'data' => [
1 => [
'A' => 'hello',
],
],
'rows' => [],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema([1 => ['A' => 'hello [world]']], ['world' => function () {
}], [])->toArray()
);
}
/**
* @return void
*/
public function test_schema_conditions1()
{
$data = [
[
'values' => [
1 => [
'A' => 'AA [$= foo] [$= bar]',
'B' => 'BB [$= baz] [$= foo] [$= bar]',
'C' => 'CC [$= foo] [$= baz] [$= bar]',
'D' => 'DD [$= foo] [$= bar] [$= baz]',
],
2 => [
'A' => 'AA [$! baz] [$! foobar]',
'B' => 'BB [$! foo] [$! baz] [$! foobar]',
'C' => 'CC [$! baz] [$! foo] [$! foobar]',
'D' => 'DD [$! baz] [$! foobar] [$! foo]',
],
],
'data' => [
'foo' => 'foo',
'bar' => 'bar',
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'A' => 'AA',
'B' => null,
'C' => null,
'D' => null,
],
2 => [
'A' => 'AA',
'B' => null,
'C' => null,
'D' => null,
],
],
'rows' => [],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"$id"
);
}
}
/**
* @return void
*/
public function test_schema_conditions2()
{
$data = [
[
'values' => [
1 => [
'A' => '11 [= foo] [= bar]',
],
2 => [
'A' => '22 [= baz] [= foo] [= bar]',
],
3 => [
'A' => '33 [= foo] [= baz] [= bar]',
],
4 => [
'A' => '44 [= foo] [= bar] [= baz]',
],
5 => [
'A' => '55 [! baz] [! foobar]',
],
6 => [
'A' => '66 [! foo] [! baz] [! foobar]',
],
7 => [
'A' => '77 [! baz] [! foo] [! foobar]',
],
8 => [
'A' => '88 [! baz] [! foobar] [! foo]',
],
],
'data' => [
'foo' => 'foo',
'bar' => 'bar',
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'A' => '11',
],
2 => [
'A' => '55',
],
],
'rows' => [
['action' => 'delete', 'row' => 2, 'qty' => 1],
['action' => 'delete', 'row' => 2, 'qty' => 1],
['action' => 'delete', 'row' => 2, 'qty' => 1],
['action' => 'delete', 'row' => 3, 'qty' => 1],
['action' => 'delete', 'row' => 3, 'qty' => 1],
['action' => 'delete', 'row' => 3, 'qty' => 1],
],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"$id"
);
}
}
/**
* @return void
*/
public function test_schema_list_zero_empty()
{
$data = [
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo] [=test]',
'C' => null,
],
2 => [
'A' => 'foo',
'B' => '[foo] [= list]',
'C' => null,
],
3 => [
'A' => 'bar [= bar]',
'B' => '[bar] 111',
'C' => null,
],
4 => [
'A' => 'foo [= list.0]',
'B' => '[foo]',
'C' => null,
],
5 => [
'A' => 'bar',
'B' => '[bar] 222 [=bar]',
'C' => '=A3+B5+C7',
],
6 => [
'A' => 'foo [= list.0.c]',
'B' => '[foo] [!list]',
'C' => null,
],
7 => [
'A' => 'bar',
'B' => '[bar] 333 [! list]',
'C' => null,
],
],
'data' => [
'list' => [],
'foo' => 'test1',
'bar' => 'test2',
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'A' => 'bar',
'B' => 'test2 111',
],
2 => [
'B' => 'test2 222',
'C' => '=A1+B2+C3',
],
3 => [
'B' => 'test2 333',
],
],
'rows' => [
['action' => 'delete', 'row' => 1, 'qty' => 1],
['action' => 'delete', 'row' => 1, 'qty' => 1],
['action' => 'delete', 'row' => 2, 'qty' => 1],
['action' => 'delete', 'row' => 3, 'qty' => 1],
],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"#$id"
);
}
}
/**
* @return void
*/
public function test_schema_list_zero()
{
$data = [
[
'values' => [
1 => [
'A' => 'foo [= list.0.c]',
'B' => '[foo]',
'K' => null,
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list.0.c]',
'D' => '[list.0.d]',
'E' => '[foo] hello [list.0.c] -> [list.0.d] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'I' => '=A1*B2*C3',
'K' => '=A2:A2',
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'I' => '=A1*B2*C3',
'K' => null,
],
],
'data' => [
'list' => [],
'foo' => 'test1',
'bar' => 'test2',
],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo] [=list_c.0]',
'K' => null,
],
2 => [
'A' => 'foo',
'B' => '[foo] [!list_c]',
'C' => '[list_c.0]',
'D' => '[list_d.0]',
'E' => '[foo] hello [list_c.0] -> [list_d.0] world [bar]',
'F' => 'bar',
'G' => '[bar] [! list_c]',
'I' => '=A1*B2*C3',
'K' => '=A2:A2',
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'I' => '=A1*B2*C3',
'K' => null,
],
],
'data' => [
'list_c' => [],
'list_d' => [],
'foo' => 'test1',
'bar' => 'test2',
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'B' => 'test1',
'C' => null,
'D' => null,
'E' => 'test1 hello -> world test2',
'G' => 'test2',
'I' => '=A1*B1*C2',
'K' => '=A1:A1',
],
'2' => [
'B' => 'test2',
'I' => '=A1*B1*C2',
],
],
'rows' => [
['action' => 'delete', 'row' => 1, 'qty' => 1],
],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"#$id"
);
}
}
/**
* @return void
*/
public function test_schema_list_one()
{
$data = [
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list.0.c]',
'D' => '[list.0.d]',
'E' => '[foo] hello [list.0.c] -> [list.0.d] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'H' => '=A1*B2*C3',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
4 => [
'A' => '[bar] [! list]',
'K' => null,
],
],
'data' => [
'list' => [ ['c' => '11', 'd' => '12'] ],
'foo' => 'test1',
'bar' => 'test2',
],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list.c]',
'D' => '[list.d]',
'E' => '[foo] hello [list.c] -> [list.d] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'H' => '=A1*B2*C3',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
4 => [
'A' => '[bar] [!list]',
'K' => null,
],
],
'data' => [
'list' => [ ['c' => '11', 'd' => '12'] ],
'foo' => 'test1',
'bar' => 'test2',
],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list_c.0]',
'D' => '[list_d.0]',
'E' => '[foo] hello [list_c.0] -> [list_d.0] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'H' => '=A1*B2*C3',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
4 => [
'A' => '[bar] [! list_c]',
'K' => null,
],
],
'data' => [
'list_c' => ['11'],
'list_d' => ['12'],
'foo' => 'test1',
'bar' => 'test2',
],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list_c]',
'D' => '[list_d]',
'E' => '[foo] hello [list_c] -> [list_d] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'H' => '=A1*B2*C3',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
4 => [
'A' => '[bar] [!list_c]',
'K' => null,
],
],
'data' => [
'list_c' => ['11'],
'list_d' => ['12'],
'foo' => 'test1',
'bar' => 'test2',
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'B' => 'test1',
'H' => '=A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'B' => 'test1',
'C' => '11',
'D' => '12',
'E' => 'test1 hello 11 -> 12 world test2',
'G' => 'test2',
'H' => '=A1*B2*C3',
],
3 => [
'B' => 'test2',
'H' => '=A1*B2*C3',
'K' => '=A2:A2',
],
],
'rows' => [['action' => 'delete', 'row' => 4, 'qty' => 1]],
'copy_style' => [],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"$id"
);
}
}
/**
* @return void
*/
public function test_schema_list_two()
{
$data = [
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list.c]',
'D' => '[list.d]',
'E' => '[foo] hello [list.c] -> [list.d] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
],
'data' => [
'list' => [ ['c' => '11', 'd' => '12'], ['c' => '21', 'd' => '22'] ],
'foo' => 'test1',
'bar' => 'test2',
],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list.c]',
'D' => '[list.d]',
'E' => '[foo] hello [list.*.c] -> [list.*.d] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
],
'data' => [
'list' => [ ['c' => '11', 'd' => '12'], ['c' => '21', 'd' => '22'] ],
'foo' => 'test1',
'bar' => 'test2',
],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list_c]',
'D' => '[list_d]',
'E' => '[foo] hello [list_c] -> [list_d] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
],
'data' => [
'list_c' => ['11', '21'],
'list_d' => ['12', '22'],
'foo' => 'test1',
'bar' => 'test2',
],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list_c.*]',
'D' => '[list_d.*]',
'E' => '[foo] hello [list_c.*] -> [list_d.*] world [bar]',
'F' => 'bar',
'G' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'H' => '=A1*B2*C3',
'J' => 'A1*B2*C3',
'K' => '=A2:A2',
],
],
'data' => [
'list_c' => ['11', '21'],
'list_d' => ['12', '22'],
'foo' => 'test1',
'bar' => 'test2',
],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'B' => 'test1',
'H' => '=A1*B2*C4',
'K' => '=A2:A3',
],
2 => [
'A' => 'foo',
'B' => 'test1',
'C' => '11',
'D' => '12',
'E' => 'test1 hello 11 -> 12 world test2',
'F' => 'bar',
'G' => 'test2',
'H' => '=A1*B2*C4',
'J' => 'A1*B2*C3',
],
3 => [
'A' => 'foo',
'B' => 'test1',
'C' => '21',
'D' => '22',
'E' => 'test1 hello 21 -> 22 world test2',
'F' => 'bar',
'G' => 'test2',
'H' => '=A1*B3*C4',
'J' => 'A1*B2*C3',
],
4 => [
'B' => 'test2',
'H' => '=A1*B2*C4',
'K' => '=A2:A3',
],
],
'rows' => [
['action' => 'add', 'row' => 3, 'qty' => 1],
],
'copy_style' => [
['from' => 'A2', 'to' => 'A3'],
['from' => 'B2', 'to' => 'B3'],
['from' => 'C2', 'to' => 'C3'],
['from' => 'D2', 'to' => 'D3'],
['from' => 'E2', 'to' => 'E3'],
['from' => 'F2', 'to' => 'F3'],
['from' => 'G2', 'to' => 'G3'],
['from' => 'H2', 'to' => 'H3'],
['from' => 'I2', 'to' => 'I3'],
['from' => 'J2', 'to' => 'J3'],
['from' => 'K2', 'to' => 'K3'],
],
'merge_cells' => [],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], [])->toArray(),
"$id"
);
}
}
/**
* @return void
*/
public function test_schema_list_three()
{
$data = [
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list.c]',
'D' => '[list.d]',
'E' => '[foo] hello [list.c] -> [list.d] world [bar]',
'G' => 'bar',
'H' => '[bar]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => '=A2:A2',
],
],
'data' => [
'list' => [ ['c' => 11, 'd' => 12.5], ['c' => '21', 'd' => '22'], ['c' => '31', 'd' => '32'] ],
'foo' => 'test1',
'bar' => 'test2',
],
'merge_cells' => ['E2:F2'],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list.*.c]',
'D' => '[list.*.d]',
'E' => '[foo] hello [list.*.c] -> [list.*.d] world [bar]',
'G' => 'bar',
'H' => '[bar]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => '=A2:A2',
],
],
'data' => [
'list' => [ ['c' => 11, 'd' => 12.5], ['c' => '21', 'd' => '22'], ['c' => '31', 'd' => '32'] ],
'foo' => 'test1',
'bar' => 'test2',
],
'merge_cells' => ['E2:F2'],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list_c]',
'D' => '[list_d]',
'E' => '[foo] hello [list_c] -> [list_d] world [bar]',
'G' => 'bar',
'H' => '[bar]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => '=A2:A2',
],
],
'data' => [
'list_c' => [11, '21', '31'],
'list_d' => [12.5, '22', '32'],
'foo' => 'test1',
'bar' => 'test2',
],
'merge_cells' => ['E2:F2'],
],
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => '=A2:A2',
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list_c.*]',
'D' => '[list_d.*]',
'E' => '[foo] hello [list_c.*] -> [list_d.*] world [bar]',
'G' => 'bar',
'H' => '[bar]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => null,
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'I' => '=A2*C2+B1+D3+E$2',
'K' => '=A2:A2',
],
],
'data' => [
'list_c' => [11, '21', '31'],
'list_d' => [12.5, '22', '32'],
'foo' => 'test1',
'bar' => 'test2',
],
'merge_cells' => ['E2:F2'],
],
];
foreach ($data as $id => $item) {
$this->assertSame(
[
'data' => [
1 => [
'B' => 'test1',
'I' => '=A2*C2+B1+D5+E$2',
'K' => '=A2:A4',
],
2 => [
'A' => 'foo',
'B' => 'test1',
'C' => 11,
'D' => 12.5,
'E' => 'test1 hello 11 -> 12.5 world test2',
'G' => 'bar',
'H' => 'test2',
'I' => '=A2*C2+B1+D5+E$2',
],
3 => [
'A' => 'foo',
'B' => 'test1',
'C' => '21',
'D' => '22',
'E' => 'test1 hello 21 -> 22 world test2',
'G' => 'bar',
'H' => 'test2',
'I' => '=A3*C3+B1+D5+E$2',
],
4 => [
'A' => 'foo',
'B' => 'test1',
'C' => '31',
'D' => '32',
'E' => 'test1 hello 31 -> 32 world test2',
'G' => 'bar',
'H' => 'test2',
'I' => '=A4*C4+B1+D5+E$2',
],
5 => [
'B' => 'test2',
'I' => '=A2*C2+B1+D5+E$2',
'K' => '=A2:A4',
],
],
'rows' => [
['action' => 'add', 'row' => 3, 'qty' => 2],
],
'copy_style' => [
['from' => 'A2', 'to' => 'A3:A4'],
['from' => 'B2', 'to' => 'B3:B4'],
['from' => 'C2', 'to' => 'C3:C4'],
['from' => 'D2', 'to' => 'D3:D4'],
['from' => 'E2', 'to' => 'E3:E4'],
['from' => 'G2', 'to' => 'G3:G4'],
['from' => 'H2', 'to' => 'H3:H4'],
['from' => 'I2', 'to' => 'I3:I4'],
['from' => 'J2', 'to' => 'J3:J4'],
['from' => 'K2', 'to' => 'K3:K4'],
],
'merge_cells' => ['E3:F3', 'E4:F4'],
'copy_width' => [],
],
$this->service->schema($item['values'], $item['data'], $item['merge_cells'])->toArray(),
"$id"
);
}
}
/**
* @return void
*/
public function test_schema_list_three_limit()
{
$data = [
[
'values' => [
1 => [
'A' => 'foo',
'B' => '[foo]',
'G' => null,
],
2 => [
'A' => 'foo',
'B' => '[foo]',
'C' => '[list.0.c]',
'D' => '[list.0.d]',
'E' => '[foo] hello [list.0.c] -> [list.0.d] world [bar]',
'F' => 'bar',
'G' => '[bar]',
],
3 => [
'A' => 'bar',
'B' => '[bar]',
'G' => null,
],
],
'data' => [
'list' => [ ['c' => 11, 'd' => 12.5], ['c' => '21', 'd' => '22'], ['c' => '31', 'd' => '32'] ],
'foo' => 'test1',
'bar' => 'test2',
],
gitextract_73d96ymn/
├── .gitattributes
├── .gitignore
├── .php-cs-fixer.php
├── LICENSE
├── README.md
├── composer.json
├── phpcs.xml
├── phpstan.neon
├── phpunit.xml
├── psalm.xml
├── src/
│ ├── Buffer.php
│ ├── DocumentService.php
│ ├── Drivers/
│ │ ├── DocumentInterface.php
│ │ ├── GridInterface.php
│ │ ├── LoadInterface.php
│ │ ├── MixInterface.php
│ │ ├── MultiSheetInterface.php
│ │ ├── PhpSpreadsheetDriver.php
│ │ ├── SaveInterface.php
│ │ ├── SheetsInterface.php
│ │ └── ZipDriver.php
│ ├── Facades/
│ │ ├── ExportGridInterface.php
│ │ ├── ExportGridQueryInterface.php
│ │ └── ExportService.php
│ ├── Format.php
│ ├── Generated.php
│ ├── GridService.php
│ ├── Mixer.php
│ ├── Sheets/
│ │ ├── Parser.php
│ │ └── SchemaMapper.php
│ ├── SheetsService.php
│ ├── Traits/
│ │ ├── Parser.php
│ │ └── XFormat.php
│ └── resources/
│ └── grid.xlsx
└── tests/
├── GridServiceTest.php
├── SheetsParserTest.php
└── TraitsTest.php
SYMBOL INDEX (249 symbols across 26 files)
FILE: src/Buffer.php
class Buffer (line 5) | class Buffer implements \Stringable
method __construct (line 23) | public function __construct(string $buffer)
method __toString (line 36) | public function __toString(): string
method __destruct (line 45) | public function __destruct()
FILE: src/DocumentService.php
class DocumentService (line 7) | class DocumentService
method __construct (line 20) | public function __construct(DocumentInterface $driver = new \AnourVala...
method generate (line 32) | public function generate(string|\Stringable $templateFile, mixed $data...
method canonizeData (line 52) | protected function canonizeData(mixed $data): array
FILE: src/Drivers/DocumentInterface.php
type DocumentInterface (line 5) | interface DocumentInterface extends SaveInterface, LoadInterface
method replace (line 13) | public function replace(array $data): self;
FILE: src/Drivers/GridInterface.php
type GridInterface (line 5) | interface GridInterface extends SaveInterface
method create (line 12) | public function create(): self;
method setGrid (line 20) | public function setGrid(iterable $data): self;
FILE: src/Drivers/LoadInterface.php
type LoadInterface (line 5) | interface LoadInterface
method load (line 14) | public function load(string $file, \AnourValar\Office\Format $format):...
FILE: src/Drivers/MixInterface.php
type MixInterface (line 5) | interface MixInterface extends MultiSheetInterface
method setSheetTitle (line 13) | public function setSheetTitle(string $title): self;
method getSheetTitle (line 20) | public function getSheetTitle(): string;
method mergeDriver (line 28) | public function mergeDriver(\AnourValar\Office\Drivers\MixInterface $d...
FILE: src/Drivers/MultiSheetInterface.php
type MultiSheetInterface (line 5) | interface MultiSheetInterface
method setSheet (line 13) | public function setSheet(int $index): self;
method getSheetCount (line 20) | public function getSheetCount(): int;
FILE: src/Drivers/PhpSpreadsheetDriver.php
class PhpSpreadsheetDriver (line 9) | class PhpSpreadsheetDriver implements SheetsInterface, GridInterface, Mi...
method sheet (line 42) | public function sheet(): \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
method create (line 52) | public function create(): self
method load (line 67) | public function load(string $file, \AnourValar\Office\Format $format):...
method save (line 81) | public function save(string $file, \AnourValar\Office\Format $format):...
method __destruct (line 104) | public function __destruct()
method setSheet (line 116) | public function setSheet(int $index): self
method getSheetCount (line 127) | public function getSheetCount(): int
method setValue (line 140) | public function setValue(string $cell, $value, bool $autoCellFormat = ...
method setValues (line 176) | public function setValues(array $data, bool $autoCellFormat = true): self
method setGrid (line 191) | public function setGrid(iterable $data): self
method getValue (line 216) | public function getValue(string $cell)
method getValues (line 225) | public function getValues(?string $ceilRange): array
method getMergeCells (line 244) | public function getMergeCells(): array
method mergeCells (line 253) | public function mergeCells(string $ceilRange): self
method copyStyle (line 264) | public function copyStyle(string $cellFrom, string $rangeTo): self
method copyCellFormat (line 279) | public function copyCellFormat(string $cellFrom, string $rangeTo): self
method setCellFormat (line 293) | public function setCellFormat(string $range, string $format): self
method addRow (line 304) | public function addRow(int $rowBefore, int $qty = 1): self
method deleteRow (line 315) | public function deleteRow(int $row, int $qty = 1): self
method addColumn (line 336) | public function addColumn(string $columnBefore, int $qty = 1): self
method autoWidth (line 349) | public function autoWidth(string $column): self
method copyWidth (line 360) | public function copyWidth(string $columnFrom, string $columnTo): self
method setWidth (line 375) | public function setWidth(string $column, int|float $width): self
method copyHeight (line 389) | public function copyHeight(int $rowFrom, int $rowTo): self
method setHeight (line 404) | public function setHeight(string $row, int|float $height): self
method setSheetTitle (line 415) | public function setSheetTitle(string $title): self
method getSheetTitle (line 426) | public function getSheetTitle(): string
method mergeDriver (line 435) | public function mergeDriver(\AnourValar\Office\Drivers\MixInterface $d...
method copyStyleWithoutFormat (line 452) | public function copyStyleWithoutFormat(string $cellFrom, string $range...
method findCell (line 488) | public function findCell($value, bool $strict = false): ?array
method duplicateRows (line 510) | public function duplicateRows(string $ceilRange, callable $value, int ...
method setStyle (line 572) | public function setStyle(string $range, array $style): self
method insertImage (line 666) | public function insertImage(string $filename, string $cell, array $opt...
method readConfiguration (line 725) | protected function readConfiguration(PhpSpreadsheetDriver $instance): ...
method writeConfiguration (line 736) | protected function writeConfiguration(\PhpOffice\PhpSpreadsheet\Writer...
method getFormat (line 747) | protected function getFormat(\AnourValar\Office\Format $format): string
FILE: src/Drivers/SaveInterface.php
type SaveInterface (line 5) | interface SaveInterface
method save (line 14) | public function save(string $file, \AnourValar\Office\Format $format):...
FILE: src/Drivers/SheetsInterface.php
type SheetsInterface (line 5) | interface SheetsInterface extends SaveInterface, LoadInterface, MultiShe...
method setValues (line 14) | public function setValues(array $data, bool $autoCellFormat = true): s...
method getValues (line 22) | public function getValues(?string $ceilRange): array;
method getMergeCells (line 29) | public function getMergeCells(): array;
method mergeCells (line 37) | public function mergeCells(string $ceilRange): self;
method copyStyle (line 46) | public function copyStyle(string $cellFrom, string $rangeTo): self;
method copyCellFormat (line 55) | public function copyCellFormat(string $cellFrom, string $rangeTo): self;
method addRow (line 64) | public function addRow(int $rowBefore, int $qty = 1): self;
method deleteRow (line 73) | public function deleteRow(int $row, int $qty = 1): self;
method copyWidth (line 82) | public function copyWidth(string $columnFrom, string $columnTo): self;
FILE: src/Drivers/ZipDriver.php
class ZipDriver (line 5) | class ZipDriver implements DocumentInterface, GridInterface
method create (line 29) | public function create(): self
method load (line 39) | public function load(string $file, \AnourValar\Office\Format $format):...
method save (line 74) | public function save(string $file, \AnourValar\Office\Format $format):...
method replace (line 105) | public function replace(array $data): self
method setGrid (line 126) | public function setGrid(iterable $data): self
method handleReplace (line 277) | protected function handleReplace(string $content, array &$data): string
method setStyle (line 308) | public function setStyle(string $column, string $style): self
method setWidth (line 322) | public function setWidth(string $column, int $width): self
method setHeight (line 336) | public function setHeight(string $row, int|float $height): self
method setSheetTitle (line 349) | public function setSheetTitle(string $title): self
method loadGridStyles (line 363) | protected function loadGridStyles(): array
method saveGridSharedStrings (line 393) | protected function saveGridSharedStrings(array &$buckets): void
method saveGridWorksheet (line 416) | protected function saveGridWorksheet(string &$cols, string &$sheet, in...
method saveGridEtc (line 461) | protected function saveGridEtc(): void
FILE: src/Facades/ExportGridInterface.php
type ExportGridInterface (line 7) | interface ExportGridInterface
method sheetTitle (line 15) | public function sheetTitle(array $request): string;
method columns (line 23) | public function columns(array $request): array;
method item (line 34) | public function item($row, GridInterface $driver, int $rowNumber, arra...
method fileName (line 43) | public function fileName(string $ext, array $request): string;
FILE: src/Facades/ExportGridQueryInterface.php
type ExportGridQueryInterface (line 24) | interface ExportGridQueryInterface extends ExportGridInterface
method query (line 31) | public function query(): \Illuminate\Database\Eloquent\Builder;
FILE: src/Facades/ExportService.php
class ExportService (line 8) | class ExportService
method grid (line 28) | public function grid(\Closure $dataGenerator, ExportGridInterface $gri...
method handleExtras (line 73) | protected function handleExtras(GridInterface $driver, string $column,...
method getDriver (line 117) | protected function getDriver(Format $format): GridInterface
FILE: src/Format.php
method fileExtension (line 17) | public function fileExtension(): string
method contentType (line 34) | public function contentType(): string
FILE: src/Generated.php
class Generated (line 5) | class Generated
method __construct (line 23) | public function __construct(\AnourValar\Office\Drivers\SaveInterface $...
method save (line 34) | public function save(Format $format): string
method saveAs (line 54) | public function saveAs(string $filename, ?Format $format = null): ?int
method hookSave (line 69) | public function hookSave(?\Closure $closure): self
FILE: src/GridService.php
class GridService (line 7) | class GridService
method __construct (line 55) | public function __construct(GridInterface $driver = new \AnourValar\Of...
method generate (line 68) | public function generate(array $headers, iterable|\Closure $data, stri...
method hookLoad (line 109) | public function hookLoad(?\Closure $closure): self
method hookBefore (line 122) | public function hookBefore(?\Closure $closure): self
method hookHeader (line 135) | public function hookHeader(?\Closure $closure): self
method hookRow (line 148) | public function hookRow(?\Closure $closure): self
method hookAfter (line 161) | public function hookAfter(?\Closure $closure): self
method getGenerator (line 180) | protected function getGenerator(
FILE: src/Mixer.php
class Mixer (line 5) | class Mixer
method __invoke (line 14) | public function __invoke(...$generated): Generated
method getTitle (line 61) | protected function getTitle(string $title, array $titles): string
FILE: src/Sheets/Parser.php
class Parser (line 5) | class Parser
method canonizeData (line 15) | public function canonizeData(mixed $data): array
method schema (line 32) | public function schema(array $values, array $data, array $mergeCells):...
method parseValues (line 61) | protected function parseValues(array $values, &$lastColumn): array
method parseData (line 81) | protected function parseData(array &$data): array
method parseMergeCells (line 100) | protected function parseMergeCells(array $mergeCells): array
method canonizeMarkers (line 118) | protected function canonizeMarkers(array &$values, array &$data): void
method calculateDataSchema (line 184) | protected function calculateDataSchema(
method shiftFormulas (line 419) | protected function shiftFormulas(array &$values, SchemaMapper &$schema...
method replaceMarkers (line 543) | protected function replaceMarkers(array &$dataSchema, array &$data, Sc...
method isShortPath (line 601) | private function isShortPath(string $path, array $markers): bool
method hasMarker (line 625) | private function hasMarker(string $marker, ?string $value): bool
method shouldBeDeleted (line 645) | private function shouldBeDeleted(array $columns, array &$data, string ...
method increment (line 692) | private function increment(string $markerName, bool $first, int $shift...
method increments (line 734) | private function increments(string $value, bool $first, int $shift = 1...
method addRow (line 756) | private function addRow(SchemaMapper &$schema, array &$mergeCells, int...
method deleteRow (line 778) | private function deleteRow(SchemaMapper &$schema, array &$mergeCells, ...
method insideMerge (line 800) | private function insideMerge(string $column, int $row, array &$mergeCe...
FILE: src/Sheets/SchemaMapper.php
class SchemaMapper (line 5) | class SchemaMapper
method toArray (line 25) | public function toArray(): array
method getOriginal (line 41) | public function getOriginal(): array
method addData (line 52) | public function addData(int $row, string $column, mixed $value): self
method addRow (line 63) | public function addRow(int $rowBefore): self
method deleteRow (line 74) | public function deleteRow(int $row): self
method copyStyle (line 86) | public function copyStyle(string $from, string $to): self
method mergeCells (line 97) | public function mergeCells(string $ceilRange): self
method copyWidth (line 109) | public function copyWidth(string $from, string $to): self
method normalizeRows (line 120) | protected function normalizeRows(array &$rows): void
method normalizeCells (line 151) | protected function normalizeCells(array &$data): void
FILE: src/SheetsService.php
class SheetsService (line 7) | class SheetsService
method __construct (line 52) | public function __construct(
method generate (line 68) | public function generate(string|\Stringable $templateFile, mixed $data...
method hookLoad (line 113) | public function hookLoad(?\Closure $closure): self
method hookBefore (line 126) | public function hookBefore(?\Closure $closure): self
method hookValue (line 139) | public function hookValue(?\Closure $closure): self
method hookAfter (line 152) | public function hookAfter(?\Closure $closure): self
method handleSheet (line 166) | protected function handleSheet(SheetsInterface &$driver, array &$data,...
method handleData (line 211) | protected function handleData(array $data, SheetsInterface $driver, in...
FILE: src/Traits/Parser.php
type Parser (line 5) | trait Parser
method dot (line 12) | protected function dot(array $data, string $prefix = ''): array
method isColumnLE (line 32) | protected function isColumnLE(string $compareColumn, string $reference...
method isColumnGE (line 53) | protected function isColumnGE(string $compareColumn, string $reference...
method strIncrement (line 75) | protected function strIncrement(string $value): string
FILE: src/Traits/XFormat.php
type XFormat (line 5) | trait XFormat
method excelDate (line 11) | protected function excelDate(\DateTimeInterface $date): float
method escape (line 46) | protected function escape(?string $value): string
FILE: tests/GridServiceTest.php
class GridServiceTest (line 5) | class GridServiceTest extends \PHPUnit\Framework\TestCase
method test_generate_statistic_with_headers (line 10) | public function test_generate_statistic_with_headers()
method test_generate_statistic_with_headers_with_shift (line 160) | public function test_generate_statistic_with_headers_with_shift()
method test_generate_statistic_without_headers (line 322) | public function test_generate_statistic_without_headers()
method test_generate_statistic_without_headers_with_shift (line 448) | public function test_generate_statistic_without_headers_with_shift()
method getDriver (line 585) | protected function getDriver(): \AnourValar\Office\Drivers\GridInterface
FILE: tests/SheetsParserTest.php
class SheetsParserTest (line 5) | class SheetsParserTest extends \PHPUnit\Framework\TestCase
method setUp (line 17) | protected function setUp(): void
method test_collision_names (line 25) | public function test_collision_names()
method test_empty (line 145) | public function test_empty()
method test_schema_scalar (line 209) | public function test_schema_scalar()
method test_schema_not_scalar (line 320) | public function test_schema_not_scalar()
method test_schema_conditions1 (line 416) | public function test_schema_conditions1()
method test_schema_conditions2 (line 478) | public function test_schema_conditions2()
method test_schema_list_zero_empty (line 554) | public function test_schema_list_zero_empty()
method test_schema_list_zero (line 643) | public function test_schema_list_zero()
method test_schema_list_one (line 757) | public function test_schema_list_one()
method test_schema_list_two (line 972) | public function test_schema_list_two()
method test_schema_list_three (line 1203) | public function test_schema_list_three()
method test_schema_list_three_limit (line 1437) | public function test_schema_list_three_limit()
method test_schema_list_four (line 1543) | public function test_schema_list_four()
method test_schema_matrix_zero_empty (line 1787) | public function test_schema_matrix_zero_empty()
method test_schema_matrix_zero (line 1888) | public function test_schema_matrix_zero()
method test_schema_matrix_one (line 2034) | public function test_schema_matrix_one()
method test_schema_matrix_two (line 2261) | public function test_schema_matrix_two()
method test_schema_matrix_two_limit (line 2410) | public function test_schema_matrix_two_limit()
method test_schema_matrix_three (line 2501) | public function test_schema_matrix_three()
method test_schema_multi_equal (line 2655) | public function test_schema_multi_equal()
method test_schema_multi_equal_limit (line 2824) | public function test_schema_multi_equal_limit()
method test_schema_multi_irr (line 2895) | public function test_schema_multi_irr()
method test_schema_multi_combination1 (line 3028) | public function test_schema_multi_combination1()
method test_schema_multi_combination1_limit (line 3170) | public function test_schema_multi_combination1_limit()
method test_schema_multi_combination2 (line 3244) | public function test_schema_multi_combination2()
method test_schema_multi_combination2_limit (line 3386) | public function test_schema_multi_combination2_limit()
method test_schema_multi_combination3 (line 3472) | public function test_schema_multi_combination3()
method test_schema_multi_combination3_limit (line 3668) | public function test_schema_multi_combination3_limit()
method test_schema_table_with_formula1 (line 3759) | public function test_schema_table_with_formula1()
method test_schema_table_with_formula2 (line 3877) | public function test_schema_table_with_formula2()
method test_schema_table_with_formula3 (line 4032) | public function test_schema_table_with_formula3()
method test_schema_matrix_merge_1x1 (line 4156) | public function test_schema_matrix_merge_1x1()
method test_schema_matrix_merge_1x2 (line 4227) | public function test_schema_matrix_merge_1x2()
method test_schema_matrix_merge_1x3 (line 4301) | public function test_schema_matrix_merge_1x3()
method test_schema_matrix_merge_3x1 (line 4378) | public function test_schema_matrix_merge_3x1()
method test_schema_matrix_merge_3x2 (line 4469) | public function test_schema_matrix_merge_3x2()
method test_schema_matrix_merge_3x3 (line 4567) | public function test_schema_matrix_merge_3x3()
method test_schema_matrix_merge_multi1 (line 4668) | public function test_schema_matrix_merge_multi1()
method test_schema_matrix_merge_multi2 (line 4765) | public function test_schema_matrix_merge_multi2()
method test_schema_list_merge_0x2 (line 4864) | public function test_schema_list_merge_0x2()
method test_schema_list_merge_1x2 (line 4936) | public function test_schema_list_merge_1x2()
method test_schema_list_merge_2x2 (line 5010) | public function test_schema_list_merge_2x2()
method test_schema_list_merge_2x2_skip (line 5103) | public function test_schema_list_merge_2x2_skip()
method test_schema_list_merge_3x2 (line 5175) | public function test_schema_list_merge_3x2()
method test_schema_list_merge_4x2 (line 5282) | public function test_schema_list_merge_4x2()
method test_schema_list_merge_4x3 (line 5403) | public function test_schema_list_merge_4x3()
method test_schema_list_merge_4x4 (line 5542) | public function test_schema_list_merge_4x4()
method test_schema_list_merge_4x5 (line 5702) | public function test_schema_list_merge_4x5()
method test_schema_list_merge_multi (line 5888) | public function test_schema_list_merge_multi()
method test_schema_multi_shift1 (line 6032) | public function test_schema_multi_shift1()
method test_schema_multi_shift2 (line 6096) | public function test_schema_multi_shift2()
method test_schema_multi_shift3 (line 6168) | public function test_schema_multi_shift3()
method test_schema_multi_shift4 (line 6223) | public function test_schema_multi_shift4()
method test_schema_multi_merge1 (line 6278) | public function test_schema_multi_merge1()
method test_schema_multi_merge2 (line 6376) | public function test_schema_multi_merge2()
method test_schema_list_long (line 6520) | public function test_schema_list_long()
method test_schema_several_tables1 (line 6631) | public function test_schema_several_tables1()
method test_schema_several_tables2 (line 6721) | public function test_schema_several_tables2()
method test_schema_several_tables3 (line 6794) | public function test_schema_several_tables3()
method test_schema_several_tables4 (line 6888) | public function test_schema_several_tables4()
method test_schema_merge1 (line 6970) | public function test_schema_merge1()
method test_schema_merge2 (line 7032) | public function test_schema_merge2()
method test_schema_merge3 (line 7092) | public function test_schema_merge3()
method test_schema_merge4 (line 7162) | public function test_schema_merge4()
method test_schema_merge5 (line 7245) | public function test_schema_merge5()
method test_schema_merge6 (line 7304) | public function test_schema_merge6()
method test_schema_alias1 (line 7372) | public function test_schema_alias1()
method test_schema_alias2 (line 7474) | public function test_schema_alias2()
method test_schema_alias3 (line 7536) | public function test_schema_alias3()
method test_schema_styles1 (line 7588) | public function test_schema_styles1()
method test_schema_styles2 (line 7644) | public function test_schema_styles2()
method test_schema_styles3 (line 7699) | public function test_schema_styles3()
method test_schema_styles4 (line 7755) | public function test_schema_styles4()
method test_schema_styles5 (line 7835) | public function test_schema_styles5()
method test_schema_styles6 (line 7904) | public function test_schema_styles6()
FILE: tests/TraitsTest.php
class TraitsTest (line 5) | class TraitsTest extends \PHPUnit\Framework\TestCase
method test_isColumnLE (line 12) | public function test_isColumnLE()
method test_isColumnGE (line 30) | public function test_isColumnGE()
method test_sort (line 48) | public function test_sort()
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (428K chars).
[
{
"path": ".gitattributes",
"chars": 208,
"preview": "/tests export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n.php-cs-fixer.php export-ignore\nphpcs.xml exp"
},
{
"path": ".gitignore",
"chars": 58,
"preview": ".phpunit.cache/\nvendor/\ncomposer.lock\n.php-cs-fixer.cache\n"
},
{
"path": ".php-cs-fixer.php",
"chars": 210,
"preview": "<?php\n\n$finder = PhpCsFixer\\Finder::create()\n ->exclude('vendor')\n ->in(__DIR__);\n\n$config = new PhpCsFixer\\Config"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2019 AnourValar\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 9415,
"preview": "# Office: Documents | Reports | Grids\n\n## Installation\n\n### Minimal\n\n```bash\ncomposer require anourvalar/office\n```\n\n###"
},
{
"path": "composer.json",
"chars": 966,
"preview": "{\n \"name\": \"anourvalar/office\",\n \"description\": \"Generate documents from existing Excel & Word templates | Export "
},
{
"path": "phpcs.xml",
"chars": 1319,
"preview": "<?xml version=\"1.0\"?>\n<!-- @see https://pear.php.net/manual/en/package.php.php-codesniffer.annotated-ruleset.php -->\n<ru"
},
{
"path": "phpstan.neon",
"chars": 2561,
"preview": "parameters:\n\n paths:\n - src\n - tests\n\n # The level 10 is the highest level\n level: 5\n\n ignoreE"
},
{
"path": "phpunit.xml",
"chars": 597,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n bootstrap=\"vendo"
},
{
"path": "psalm.xml",
"chars": 1665,
"preview": "<?xml version=\"1.0\"?>\n<psalm\n errorLevel=\"7\"\n resolveFromConfigFile=\"true\"\n xmlns:xsi=\"http://www.w3.org/2001/X"
},
{
"path": "src/Buffer.php",
"chars": 909,
"preview": "<?php\n\nnamespace AnourValar\\Office;\n\nclass Buffer implements \\Stringable\n{\n /**\n * @var resource\n */\n prot"
},
{
"path": "src/DocumentService.php",
"chars": 1656,
"preview": "<?php\n\nnamespace AnourValar\\Office;\n\nuse AnourValar\\Office\\Drivers\\DocumentInterface;\n\nclass DocumentService\n{\n use \\"
},
{
"path": "src/Drivers/DocumentInterface.php",
"chars": 266,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface DocumentInterface extends SaveInterface, LoadInterface\n{\n /**\n"
},
{
"path": "src/Drivers/GridInterface.php",
"chars": 341,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface GridInterface extends SaveInterface\n{\n /**\n * Create new d"
},
{
"path": "src/Drivers/LoadInterface.php",
"chars": 361,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface LoadInterface\n{\n /**\n * Load a template with specific form"
},
{
"path": "src/Drivers/MixInterface.php",
"chars": 648,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface MixInterface extends MultiSheetInterface\n{\n /**\n * Set tit"
},
{
"path": "src/Drivers/MultiSheetInterface.php",
"chars": 328,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface MultiSheetInterface\n{\n /**\n * Set active sheet\n *\n "
},
{
"path": "src/Drivers/PhpSpreadsheetDriver.php",
"chars": 22523,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\nuse PhpOffice\\PhpSpreadsheet\\Cell\\DataType;\nuse PhpOffice\\PhpSpreadsheet\\IO"
},
{
"path": "src/Drivers/SaveInterface.php",
"chars": 303,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface SaveInterface\n{\n /**\n * Save in specific format\n *\n "
},
{
"path": "src/Drivers/SheetsInterface.php",
"chars": 1747,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\ninterface SheetsInterface extends SaveInterface, LoadInterface, MultiSheetI"
},
{
"path": "src/Drivers/ZipDriver.php",
"chars": 14724,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Drivers;\n\nclass ZipDriver implements DocumentInterface, GridInterface\n{\n use \\Anou"
},
{
"path": "src/Facades/ExportGridInterface.php",
"chars": 915,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Facades;\n\nuse AnourValar\\Office\\Drivers\\GridInterface;\n\ninterface ExportGridInterface"
},
{
"path": "src/Facades/ExportGridQueryInterface.php",
"chars": 1049,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Facades;\n\n/**\n * Usage example:\n *\n * if (! in_array($format, [\\AnourValar\\Office\\For"
},
{
"path": "src/Facades/ExportService.php",
"chars": 4609,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Facades;\n\nuse AnourValar\\Office\\Drivers\\GridInterface;\nuse AnourValar\\Office\\Format;\n"
},
{
"path": "src/Format.php",
"chars": 1305,
"preview": "<?php\n\nnamespace AnourValar\\Office;\n\nenum Format: string\n{\n case Xlsx = 'xlsx'; // sheets | grid => reader + writer\n "
},
{
"path": "src/Generated.php",
"chars": 1741,
"preview": "<?php\n\nnamespace AnourValar\\Office;\n\nclass Generated\n{\n /**\n * @var \\AnourValar\\Office\\Drivers\\SaveInterface\n "
},
{
"path": "src/GridService.php",
"chars": 8374,
"preview": "<?php\n\nnamespace AnourValar\\Office;\n\nuse AnourValar\\Office\\Drivers\\GridInterface;\n\nclass GridService\n{\n use \\AnourVal"
},
{
"path": "src/Mixer.php",
"chars": 2377,
"preview": "<?php\n\nnamespace AnourValar\\Office;\n\nclass Mixer\n{\n /**\n * Mix generated documents\n *\n * @param \\AnourVal"
},
{
"path": "src/Sheets/Parser.php",
"chars": 26234,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Sheets;\n\nclass Parser\n{\n use \\AnourValar\\Office\\Traits\\Parser;\n\n /**\n * Han"
},
{
"path": "src/Sheets/SchemaMapper.php",
"chars": 4650,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Sheets;\n\nclass SchemaMapper\n{\n /**\n * @var array\n */\n protected array $"
},
{
"path": "src/SheetsService.php",
"chars": 6538,
"preview": "<?php\n\nnamespace AnourValar\\Office;\n\nuse AnourValar\\Office\\Drivers\\SheetsInterface;\n\nclass SheetsService\n{\n /**\n "
},
{
"path": "src/Traits/Parser.php",
"chars": 1932,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Traits;\n\ntrait Parser\n{\n /**\n * @param array $data\n * @param string $prefi"
},
{
"path": "src/Traits/XFormat.php",
"chars": 1351,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Traits;\n\ntrait XFormat\n{\n /**\n * @param \\DateTimeInterface $date\n * @retur"
},
{
"path": "tests/GridServiceTest.php",
"chars": 26047,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Tests;\n\nclass GridServiceTest extends \\PHPUnit\\Framework\\TestCase\n{\n /**\n * @r"
},
{
"path": "tests/SheetsParserTest.php",
"chars": 260288,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Tests;\n\nclass SheetsParserTest extends \\PHPUnit\\Framework\\TestCase\n{\n /**\n * @"
},
{
"path": "tests/TraitsTest.php",
"chars": 1769,
"preview": "<?php\n\nnamespace AnourValar\\Office\\Tests;\n\nclass TraitsTest extends \\PHPUnit\\Framework\\TestCase\n{\n use \\AnourValar\\Of"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the AnourValar/office GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (401.4 KB), approximately 97.8k tokens, and a symbol index with 249 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.