mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-07-06 17:31:45 +00:00
Compare commits
5 commits
233c5e8550
...
97a74815d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a74815d3 | ||
|
|
7998cdcd71 | ||
|
|
5e9f7a11a3 | ||
|
|
1c6bf3f472 | ||
|
|
aed2652f1d |
7 changed files with 88 additions and 35 deletions
|
|
@ -91,18 +91,20 @@ in [official documentation](https://twig.symfony.com/doc/3.x/).
|
||||||
|
|
||||||
Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create
|
Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create
|
||||||
the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment,
|
the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment,
|
||||||
where only certain operations are allowed. Only read access to entities is allowed. However as it circumvents Part-DB normal permission system,
|
where only certain operations are allowed. Only read access to entities is allowed. However, as it circumvents Part-DB normal permission system,
|
||||||
the twig mode is only available to users with the "Twig mode" permission.
|
the twig mode is only available to users with the "Twig mode" permission.
|
||||||
|
|
||||||
|
It is useful to use the HTML embed feature of the editor, to have a block where you can write the twig code without worrying about the WYSIWYG editor messing with your code.
|
||||||
|
|
||||||
The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` (
|
The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` (
|
||||||
or `{% raw %}{{ variable.property }}{% endraw %}`):
|
or `{% raw %}{{ variable.property }}{% endraw %}`):
|
||||||
|
|
||||||
| Variable name | Description |
|
| Variable name | Description |
|
||||||
|--------------------------------------------|--------------------------------------------------------------------------------------|
|
|--------------------------------------------|--------------------------------------------------------------------------------------|
|
||||||
| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. |
|
| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. |
|
||||||
| `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in |
|
| `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in |
|
||||||
| `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). |
|
| `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). |
|
||||||
| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated |
|
| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated ) |
|
||||||
| `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels |
|
| `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels |
|
||||||
| `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm |
|
| `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm |
|
||||||
| `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm |
|
| `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm |
|
||||||
|
|
@ -236,12 +238,18 @@ certain data:
|
||||||
|
|
||||||
#### Functions
|
#### Functions
|
||||||
|
|
||||||
| Function name | Description |
|
| Function name | Description |
|
||||||
|----------------------------------------------|-----------------------------------------------------------------------------------------------|
|
|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `placeholder(placeholder, element)` | Get the value of a placeholder of an element |
|
| `placeholder(placeholder, element)` | Get the value of a placeholder of an element |
|
||||||
| `entity_type(element)` | Get the type of an entity as string |
|
| `entity_type(element)` | Get the type of an entity as string |
|
||||||
| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) |
|
| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) |
|
||||||
| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. |
|
| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. |
|
||||||
|
| `associated_parts(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. Only the directly associated parts are returned |
|
||||||
|
| `associated_parts_r(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. including all sub-entities recursively (e.g. sub-locations) |
|
||||||
|
| `associated_parts_count(element)` | Get the count of associated parts of an element like a storagelocation, footprint, excluding sub-entities |
|
||||||
|
| `associated_parts_count_r(element)` | Get the count of associated parts of an element like a storagelocation, footprint, including all sub-entities recursively (e.g. sub-locations) |
|
||||||
|
| `type_label(element)` | Get the name of the type of an element (e.g. "Part", "Storage location", etc.) |
|
||||||
|
| `type_label_p(element)` | Get the name of the type of an element in plural form (e.g. "Parts", "Storage locations", etc.) |
|
||||||
|
|
||||||
### Filters
|
### Filters
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,15 +42,14 @@ declare(strict_types=1);
|
||||||
namespace App\Exceptions;
|
namespace App\Exceptions;
|
||||||
|
|
||||||
use RuntimeException;
|
use RuntimeException;
|
||||||
use Twig\Error\Error;
|
|
||||||
|
|
||||||
class TwigModeException extends RuntimeException
|
class TwigModeException extends RuntimeException
|
||||||
{
|
{
|
||||||
private const PROJECT_PATH = __DIR__ . '/../../';
|
private const PROJECT_PATH = __DIR__ . '/../../';
|
||||||
|
|
||||||
public function __construct(?Error $previous = null)
|
public function __construct(?\Throwable $previous = null)
|
||||||
{
|
{
|
||||||
parent::__construct($previous->getMessage(), 0, $previous);
|
parent::__construct($previous?->getMessage() ?? "Unknown message", 0, $previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -266,11 +266,14 @@ class EntityExporter
|
||||||
//Sanitize the filename
|
//Sanitize the filename
|
||||||
$filename = FilenameSanatizer::sanitizeFilename($filename);
|
$filename = FilenameSanatizer::sanitizeFilename($filename);
|
||||||
|
|
||||||
|
//Remove percent for fallback
|
||||||
|
$fallback = str_replace("%", "_", $filename);
|
||||||
|
|
||||||
// Create the disposition of the file
|
// Create the disposition of the file
|
||||||
$disposition = $response->headers->makeDisposition(
|
$disposition = $response->headers->makeDisposition(
|
||||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||||
$filename,
|
$filename,
|
||||||
u($filename)->ascii()->toString(),
|
$fallback,
|
||||||
);
|
);
|
||||||
// Set the content disposition
|
// Set the content disposition
|
||||||
$response->headers->set('Content-Disposition', $disposition);
|
$response->headers->set('Content-Disposition', $disposition);
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@ final class LabelHTMLGenerator
|
||||||
'paper_height' => $options->getHeight(),
|
'paper_height' => $options->getHeight(),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (Error $exception) {
|
} catch (\Throwable $exception) {
|
||||||
throw new TwigModeException($exception);
|
throw new TwigModeException($exception);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -86,11 +86,11 @@ use Twig\Sandbox\SecurityPolicyInterface;
|
||||||
*/
|
*/
|
||||||
final class SandboxedTwigFactory
|
final class SandboxedTwigFactory
|
||||||
{
|
{
|
||||||
private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'verbatim', 'with'];
|
private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'types', 'verbatim', 'with'];
|
||||||
private const ALLOWED_FILTERS = ['abs', 'batch', 'capitalize', 'column', 'country_name',
|
private const ALLOWED_FILTERS = ['abs', 'batch', 'capitalize', 'column', 'country_name',
|
||||||
'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'first', 'format',
|
'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'find', 'first', 'format',
|
||||||
'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'html_to_markdown', 'join', 'keys',
|
'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'html_to_markdown', 'join', 'keys',
|
||||||
'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'raw', 'number_format',
|
'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'number_format', 'raw',
|
||||||
'reduce', 'replace', 'reverse', 'round', 'slice', 'slug', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title',
|
'reduce', 'replace', 'reverse', 'round', 'slice', 'slug', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title',
|
||||||
'trim', 'u', 'upper', 'url_encode',
|
'trim', 'u', 'upper', 'url_encode',
|
||||||
|
|
||||||
|
|
@ -104,16 +104,17 @@ final class SandboxedTwigFactory
|
||||||
];
|
];
|
||||||
|
|
||||||
private const ALLOWED_FUNCTIONS = ['country_names', 'country_timezones', 'currency_names', 'cycle',
|
private const ALLOWED_FUNCTIONS = ['country_names', 'country_timezones', 'currency_names', 'cycle',
|
||||||
'date', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names',
|
'date', 'enum', 'enum_cases', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names',
|
||||||
'template_from_string', 'timezone_names',
|
'timezone_names',
|
||||||
|
|
||||||
//Part-DB specific extensions:
|
//Part-DB specific extensions:
|
||||||
//EntityExtension:
|
//EntityExtension:
|
||||||
'entity_type', 'entity_url',
|
'entity_type', 'entity_url', 'type_label', 'type_label_plural',
|
||||||
//BarcodeExtension:
|
//BarcodeExtension:
|
||||||
'barcode_svg',
|
'barcode_svg',
|
||||||
//SandboxedLabelExtension
|
//SandboxedLabelExtension
|
||||||
'placeholder',
|
'placeholder',
|
||||||
|
'associated_parts', 'associated_parts_count', 'associated_parts_r', 'associated_parts_count_r',
|
||||||
];
|
];
|
||||||
|
|
||||||
private const ALLOWED_METHODS = [
|
private const ALLOWED_METHODS = [
|
||||||
|
|
@ -130,7 +131,7 @@ final class SandboxedTwigFactory
|
||||||
'getValueTypical', 'getUnit', 'getValueText', ],
|
'getValueTypical', 'getUnit', 'getValueText', ],
|
||||||
MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'],
|
MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'],
|
||||||
PartLot::class => ['isExpired', 'getDescription', 'getComment', 'getExpirationDate', 'getStorageLocation',
|
PartLot::class => ['isExpired', 'getDescription', 'getComment', 'getExpirationDate', 'getStorageLocation',
|
||||||
'getPart', 'isInstockUnknown', 'getAmount', 'getNeedsRefill', 'getVendorBarcode'],
|
'getPart', 'isInstockUnknown', 'getAmount', 'getOwner', 'getLastStocktakeAt', 'getNeedsRefill', 'getVendorBarcode'],
|
||||||
StorageLocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'],
|
StorageLocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'],
|
||||||
Supplier::class => ['getShippingCosts', 'getDefaultCurrency'],
|
Supplier::class => ['getShippingCosts', 'getDefaultCurrency'],
|
||||||
Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference',
|
Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference',
|
||||||
|
|
@ -141,13 +142,13 @@ final class SandboxedTwigFactory
|
||||||
'getParameters', 'getGroupedParameters',
|
'getParameters', 'getGroupedParameters',
|
||||||
'isProjectBuildPart', 'getBuiltProject',
|
'isProjectBuildPart', 'getBuiltProject',
|
||||||
'getAssociatedPartsAsOwner', 'getAssociatedPartsAsOther', 'getAssociatedPartsAll',
|
'getAssociatedPartsAsOwner', 'getAssociatedPartsAsOther', 'getAssociatedPartsAll',
|
||||||
'getEdaInfo'
|
'getEdaInfo', 'getGtin'
|
||||||
],
|
],
|
||||||
Currency::class => ['getIsoCode', 'getInverseExchangeRate', 'getExchangeRate'],
|
Currency::class => ['getIsoCode', 'getInverseExchangeRate', 'getExchangeRate'],
|
||||||
Orderdetail::class => ['getPart', 'getSupplier', 'getSupplierPartNr', 'getObsolete',
|
Orderdetail::class => ['getPart', 'getSupplier', 'getSupplierPartNr', 'getObsolete',
|
||||||
'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl'],
|
'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl', 'getPricesIncludesVAT'],
|
||||||
Pricedetail::class => ['getOrderdetail', 'getPrice', 'getPricePerUnit', 'getPriceRelatedQuantity',
|
Pricedetail::class => ['getOrderdetail', 'getPrice', 'getPricePerUnit', 'getPriceRelatedQuantity',
|
||||||
'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode'],
|
'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode', 'getIncludesVat'],
|
||||||
InfoProviderReference:: class => ['getProviderKey', 'getProviderId', 'getProviderUrl', 'getLastUpdated', 'isProviderCreated'],
|
InfoProviderReference:: class => ['getProviderKey', 'getProviderId', 'getProviderUrl', 'getLastUpdated', 'isProviderCreated'],
|
||||||
PartAssociation::class => ['getType', 'getComment', 'getOwner', 'getOther', 'getOtherType'],
|
PartAssociation::class => ['getType', 'getComment', 'getOwner', 'getOther', 'getOtherType'],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,14 +23,18 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Twig\Sandbox;
|
namespace App\Twig\Sandbox;
|
||||||
|
|
||||||
|
use App\Entity\Base\AbstractPartsContainingDBElement;
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Repository\AbstractPartsContainingRepository;
|
||||||
use App\Services\LabelSystem\LabelTextReplacer;
|
use App\Services\LabelSystem\LabelTextReplacer;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Twig\Extension\AbstractExtension;
|
use Twig\Extension\AbstractExtension;
|
||||||
use Twig\TwigFilter;
|
use Twig\TwigFilter;
|
||||||
use Twig\TwigFunction;
|
use Twig\TwigFunction;
|
||||||
|
|
||||||
class SandboxedLabelExtension extends AbstractExtension
|
class SandboxedLabelExtension extends AbstractExtension
|
||||||
{
|
{
|
||||||
public function __construct(private readonly LabelTextReplacer $labelTextReplacer)
|
public function __construct(private readonly LabelTextReplacer $labelTextReplacer, private readonly EntityManagerInterface $em)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -39,6 +43,11 @@ class SandboxedLabelExtension extends AbstractExtension
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
new TwigFunction('placeholder', fn(string $text, object $label_target) => $this->labelTextReplacer->handlePlaceholderOrReturnNull($text, $label_target)),
|
new TwigFunction('placeholder', fn(string $text, object $label_target) => $this->labelTextReplacer->handlePlaceholderOrReturnNull($text, $label_target)),
|
||||||
|
|
||||||
|
new TwigFunction("associated_parts", $this->associatedParts(...)),
|
||||||
|
new TwigFunction("associated_parts_count", $this->associatedPartsCount(...)),
|
||||||
|
new TwigFunction("associated_parts_r", $this->associatedPartsRecursive(...)),
|
||||||
|
new TwigFunction("associated_parts_count_r", $this->associatedPartsCountRecursive(...)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,4 +57,37 @@ class SandboxedLabelExtension extends AbstractExtension
|
||||||
new TwigFilter('placeholders', fn(string $text, object $label_target) => $this->labelTextReplacer->replace($text, $label_target)),
|
new TwigFilter('placeholders', fn(string $text, object $label_target) => $this->labelTextReplacer->replace($text, $label_target)),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all parts associated with the given element.
|
||||||
|
* @param AbstractPartsContainingDBElement $element
|
||||||
|
* @return Part[]
|
||||||
|
*/
|
||||||
|
public function associatedParts(AbstractPartsContainingDBElement $element): array
|
||||||
|
{
|
||||||
|
/** @var AbstractPartsContainingRepository $repo */
|
||||||
|
$repo = $this->em->getRepository($element::class);
|
||||||
|
return $repo->getParts($element);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function associatedPartsCount(AbstractPartsContainingDBElement $element): int
|
||||||
|
{
|
||||||
|
/** @var AbstractPartsContainingRepository $repo */
|
||||||
|
$repo = $this->em->getRepository($element::class);
|
||||||
|
return $repo->getPartsCount($element);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function associatedPartsRecursive(AbstractPartsContainingDBElement $element): array
|
||||||
|
{
|
||||||
|
/** @var AbstractPartsContainingRepository $repo */
|
||||||
|
$repo = $this->em->getRepository($element::class);
|
||||||
|
return $repo->getPartsRecursive($element);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function associatedPartsCountRecursive(AbstractPartsContainingDBElement $element): int
|
||||||
|
{
|
||||||
|
/** @var AbstractPartsContainingRepository $repo */
|
||||||
|
$repo = $this->em->getRepository($element::class);
|
||||||
|
return $repo->getPartsCountRecursive($element);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,9 +43,9 @@ final class EntityExporterTest extends WebTestCase
|
||||||
|
|
||||||
private function getEntities(): array
|
private function getEntities(): array
|
||||||
{
|
{
|
||||||
$entity1 = (new Category())->setName('Enitity 1')->setComment('Test');
|
$entity1 = (new Category())->setName('Entity%1')->setComment('Test');
|
||||||
$entity1_1 = (new Category())->setName('Enitity 1.1')->setParent($entity1);
|
$entity1_1 = (new Category())->setName('Entity 1.1')->setParent($entity1);
|
||||||
$entity2 = (new Category())->setName('Enitity 2');
|
$entity2 = (new Category())->setName('Entity 2');
|
||||||
|
|
||||||
return [$entity1, $entity1_1, $entity2];
|
return [$entity1, $entity1_1, $entity2];
|
||||||
}
|
}
|
||||||
|
|
@ -55,12 +55,12 @@ final class EntityExporterTest extends WebTestCase
|
||||||
$entities = $this->getEntities();
|
$entities = $this->getEntities();
|
||||||
|
|
||||||
$json_without_children = $this->service->exportEntities($entities, ['format' => 'json', 'level' => 'simple']);
|
$json_without_children = $this->service->exportEntities($entities, ['format' => 'json', 'level' => 'simple']);
|
||||||
$this->assertJsonStringEqualsJsonString('[{"name":"Enitity 1","type":"category","full_name":"Enitity 1"},{"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"},{"name":"Enitity 2","type":"category","full_name":"Enitity 2"}]',
|
$this->assertJsonStringEqualsJsonString('[{"name":"Entity%1","type":"category","full_name":"Entity%1"},{"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"},{"name":"Entity 2","type":"category","full_name":"Entity 2"}]',
|
||||||
$json_without_children);
|
$json_without_children);
|
||||||
|
|
||||||
$json_with_children = $this->service->exportEntities($entities,
|
$json_with_children = $this->service->exportEntities($entities,
|
||||||
['format' => 'json', 'level' => 'simple', 'include_children' => true]);
|
['format' => 'json', 'level' => 'simple', 'include_children' => true]);
|
||||||
$this->assertJsonStringEqualsJsonString('[{"children":[{"children":[],"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"}],"name":"Enitity 1","type":"category","full_name":"Enitity 1"},{"children":[],"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"},{"children":[],"name":"Enitity 2","type":"category","full_name":"Enitity 2"}]',
|
$this->assertJsonStringEqualsJsonString('[{"children":[{"children":[],"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"}],"name":"Entity%1","type":"category","full_name":"Entity%1"},{"children":[],"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"},{"children":[],"name":"Entity 2","type":"category","full_name":"Entity 2"}]',
|
||||||
$json_with_children);
|
$json_with_children);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,8 +95,8 @@ final class EntityExporterTest extends WebTestCase
|
||||||
$this->assertSame('name', $worksheet->getCell('A1')->getValue());
|
$this->assertSame('name', $worksheet->getCell('A1')->getValue());
|
||||||
$this->assertSame('full_name', $worksheet->getCell('B1')->getValue());
|
$this->assertSame('full_name', $worksheet->getCell('B1')->getValue());
|
||||||
|
|
||||||
$this->assertSame('Enitity 1', $worksheet->getCell('A2')->getValue());
|
$this->assertSame('Entity%1', $worksheet->getCell('A2')->getValue());
|
||||||
$this->assertSame('Enitity 1', $worksheet->getCell('B2')->getValue());
|
$this->assertSame('Entity%1', $worksheet->getCell('B2')->getValue());
|
||||||
|
|
||||||
unlink($tempFile);
|
unlink($tempFile);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue