From a706885f219fd026b3eff086913af90387ff3495 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:13:37 -0300 Subject: [PATCH 1/2] feat: implement psalm Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/psalm.yml | 43 ++++++++++++++++++++++++++++++++++ composer.json | 2 ++ psalm.xml | 25 ++++++++++++++++++++ tests/psalm-baseline.xml | 2 ++ vendor-bin/psalm/composer.json | 10 ++++++++ 5 files changed, 82 insertions(+) create mode 100644 .github/workflows/psalm.yml create mode 100644 psalm.xml create mode 100644 tests/psalm-baseline.xml create mode 100644 vendor-bin/psalm/composer.json diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml new file mode 100644 index 0000000..2765291 --- /dev/null +++ b/.github/workflows/psalm.yml @@ -0,0 +1,43 @@ +name: Static analysis + +on: pull_request + +concurrency: + group: psalm-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + static-analysis: + runs-on: ubuntu-latest + strategy: + matrix: + php: + - "8.1" + - "8.2" + - "8.3" + - "8.4" + + name: static-psalm-analysis + steps: + - name: Checkout + uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + + - name: Set up php${{ matrix.php }} + uses: shivammathur/setup-php@ec406be512d7077f68eed36e63f4d91bc006edc4 # v2.35.4 + with: + php-version: ${{ matrix.php }} + coverage: none + ini-file: development + # Temporary workaround for missing pcntl_* in PHP 8.3 + ini-values: disable_functions= + + - name: Install dependencies + run: composer install + + - name: Run coding standards check + run: vendor/bin/psalm --no-cache --threads=$(nproc) --output-format=github diff --git a/composer.json b/composer.json index 9e8b5e9..4ed80bb 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,8 @@ "@composer bin all install --ansi", "composer dump-autoload" ], + "psalm": "psalm --no-cache --threads=$(nproc)", + "psalm:update-baseline": "psalm --threads=$(nproc) --update-baseline --set-baseline=tests/psalm-baseline.xml", "post-update-cmd": [ "composer dump-autoload" ], diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..a57485f --- /dev/null +++ b/psalm.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml new file mode 100644 index 0000000..d04c5aa --- /dev/null +++ b/tests/psalm-baseline.xml @@ -0,0 +1,2 @@ + + diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json new file mode 100644 index 0000000..0dc7359 --- /dev/null +++ b/vendor-bin/psalm/composer.json @@ -0,0 +1,10 @@ +{ + "require-dev": { + "vimeo/psalm": "^5.26" + }, + "config": { + "platform": { + "php": "8.1" + } + } +} From 9a65b50a268e12db4f45ccdd63fbf871ca850fa2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Thu, 28 Aug 2025 14:22:57 -0300 Subject: [PATCH 2/2] fix: apply fixes reported by psalm Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- src/Command.php | 77 +++++++++++++----------- src/DataFields.php | 30 +++++---- src/FdfFile.php | 9 ++- src/InfoFields.php | 29 ++++----- src/InfoFile.php | 27 ++++++--- src/Pdf.php | 147 +++++++++++++++++++++++++-------------------- src/XfdfFile.php | 46 ++++++++------ 7 files changed, 203 insertions(+), 162 deletions(-) diff --git a/src/Command.php b/src/Command.php index aa9f886..16d9192 100644 --- a/src/Command.php +++ b/src/Command.php @@ -3,6 +3,8 @@ namespace mikehaertl\pdftk; use mikehaertl\shellcommand\Command as BaseCommand; +use mikehaertl\tmp\File; +use Override; /** * Command @@ -16,38 +18,35 @@ class Command extends BaseCommand { /** - * @var string the pdftk binary + * The pdftk binary */ protected $_command = 'pdftk'; /** - * @var array list of input files to process as array('name' => $filename, + * List of input files to process as array('name' => $filename, * 'password' => $pw) indexed by handle */ - protected $_files = array(); + protected array $_files = array(); /** - * @var array list of command options, either strings or array with - * arguments to addArg() + * List of command options, either strings or array with arguments to addArg() */ - protected $_options = array(); + protected array $_options = array(); /** - * @var string the operation to perform + * The operation to perform */ - protected $_operation; + protected ?string $_operation = null; /** - * @var string|array operation arguments, e.g. a list of page ranges or a - * filename or tmp file instance + * Operation arguments, e.g. a list of page ranges or a filename or tmp file instance */ - protected $_operationArgument = array(); + protected string|array|File $_operationArgument = array(); /** - * @var bool whether to force escaping of the operation argument e.g. for - * filenames + * Whether to force escaping of the operation argument e.g. for filenames */ - protected $_escapeOperationArgument = false; + protected bool $_escapeOperationArgument = false; /** * @param string $name the PDF file to add for processing @@ -57,7 +56,7 @@ class Command extends BaseCommand * @return Command the command instance for method chaining * @throws \Exception */ - public function addFile($name, $handle, $password = null) + public function addFile($name, $handle, $password = null): self { $this->checkExecutionStatus(); $file = array( @@ -76,7 +75,7 @@ public function addFile($name, $handle, $password = null) * use Command default setting. * @return Command the command instance for method chaining */ - public function addOption($option, $argument = null, $escape = null) + public function addOption($option, $argument = null, ?bool $escape = null): self { $this->_options[] = $argument === null ? $option : array($option, $argument, $escape); return $this; @@ -86,7 +85,7 @@ public function addOption($option, $argument = null, $escape = null) * @param string $operation the operation to perform * @return Command the command instance for method chaining */ - public function setOperation($operation) + public function setOperation($operation): self { $this->checkExecutionStatus(); $this->_operation = $operation; @@ -102,11 +101,11 @@ public function getOperation() } /** - * @param string $value the operation argument + * @param string|array $value the operation argument * @param bool $escape whether to escape the operation argument * @return Command the command instance for method chaining */ - public function setOperationArgument($value, $escape = false) + public function setOperationArgument(string|array|File $value, bool $escape = false): self { $this->checkExecutionStatus(); $this->_operationArgument = $value; @@ -118,7 +117,7 @@ public function setOperationArgument($value, $escape = false) * @return string|array|null the current operation argument as string or * array or null if none set */ - public function getOperationArgument() + public function getOperationArgument(): string|array|null { // Typecast to string in case we have a File instance as argument return is_array($this->_operationArgument) ? $this->_operationArgument : (string) $this->_operationArgument; @@ -127,7 +126,7 @@ public function getOperationArgument() /** * @return int the number of files added to the command */ - public function getFileCount() + public function getFileCount(): int { return count($this->_files); } @@ -144,17 +143,20 @@ public function getFileCount() * only a single file was added. * @param string|null $qualifier the page number qualifier, either 'even' * or 'odd' or null for none - * @param string $rotation the rotation to apply to the pages. + * @param string|null $rotation the rotation to apply to the pages. * @return Command the command instance for method chaining */ - public function addPageRange($start, $end = null, $handle = null, $qualifier = null, $rotation = null) - { + public function addPageRange( + int|string|array $start, + int|string|null $end = null, + string|null $handle = null, + string|null $qualifier = null, + string|null $rotation = null, + ) { $this->checkExecutionStatus(); if (is_array($start)) { if ($handle !== null) { - $start = array_map(function ($p) use ($handle) { - return $handle . $p; - }, $start); + $start = array_map(fn ($p) => $handle . $p, $start); } $range = implode(' ', $start); } else { @@ -164,6 +166,13 @@ public function addPageRange($start, $end = null, $handle = null, $qualifier = n } $range .= $qualifier . $rotation; } + if (!is_array($this->_operationArgument)) { + if (!empty($this->_operationArgument)) { + $this->_operationArgument = array($this->_operationArgument); + } else { + $this->_operationArgument = array(); + } + } $this->_operationArgument[] = $range; return $this; } @@ -173,6 +182,7 @@ public function addPageRange($start, $end = null, $handle = null, $qualifier = n * null if none * @return bool whether the command was executed successfully */ + #[Override] public function execute($filename = null) { $this->checkExecutionStatus(); @@ -185,7 +195,7 @@ public function execute($filename = null) /** * Process input PDF files and create respective command arguments */ - protected function processInputFiles() + protected function processInputFiles(): void { $passwords = array(); foreach ($this->_files as $handle => $file) { @@ -204,10 +214,11 @@ protected function processInputFiles() /** * Process options and create respective command arguments + * * @param string|null $filename if provided an 'output' option will be * added */ - protected function processOptions($filename = null) + protected function processOptions($filename = null): void { // output must be first option after operation if ($filename !== null) { @@ -225,13 +236,10 @@ protected function processOptions($filename = null) /** * Process opearation and create respective command arguments */ - protected function processOperation() + protected function processOperation(): void { if ($this->_operation !== null) { $value = $this->_operationArgument ? $this->_operationArgument : null; - if ($value instanceof TmpFile) { - $value = (string) $value; - } $this->addArg($this->_operation, $value, $this->_escapeOperationArgument); } } @@ -239,9 +247,10 @@ protected function processOperation() /** * Ensure that the command was not exectued yet. Throws exception * otherwise. + * * @throws \Exception */ - protected function checkExecutionStatus() + protected function checkExecutionStatus(): void { if ($this->getExecuted()) { throw new \Exception('Operation was already executed'); diff --git a/src/DataFields.php b/src/DataFields.php index a5b40f7..5a97e06 100644 --- a/src/DataFields.php +++ b/src/DataFields.php @@ -11,11 +11,12 @@ * @author Ray Holland * @author Michael Härtl * @license http://www.opensource.org/licenses/MIT + * @extends ArrayObject */ class DataFields extends ArrayObject { - private $_string; - private $_array; + private string $_string; + private array $_array; /** * DataFields constructor. @@ -24,26 +25,23 @@ class DataFields extends ArrayObject * @param int $flags * @param string $iterator_class */ - public function __construct($input = null, $flags = 0, $iterator_class = "ArrayIterator") - { + public function __construct( + ?string $input = null, + int $flags = 0, + string $iterator_class = "ArrayIterator", + ) { $this->_string = $input ?: ''; $this->_array = self::parse($this->_string); - return parent::__construct($this->_array, $flags, $iterator_class); + parent::__construct($this->_array, $flags, $iterator_class); } - /** - * @return string - */ - public function __toString() + public function __toString(): string { return $this->_string; } - /** - * @return array - */ - public function __toArray() + public function __toArray(): array { return $this->_array; } @@ -78,7 +76,7 @@ public function __toArray() * @param $input the string to parse * @return array the parsed result */ - public static function parse($input) + public static function parse(string $input): array { if (strncmp('---', $input, 3) === 0) { // Split blocks only if '---' is followed by 'FieldType' @@ -105,7 +103,7 @@ public static function parse($input) * @param string $block the block to parse * @return array the parsed block values indexed by respective names */ - public static function parseBlock($block) + public static function parseBlock(string $block): array { $data = array(); $lines = preg_split("/(\r\n|\n|\r)/", trim($block)); @@ -156,7 +154,7 @@ public static function parseBlock($block) * 'FieldValueDefault' can span multiple lines * @return bool whether the value continues in line n + 1 */ - protected static function lineContinues($lines, $n, $key) + protected static function lineContinues(array $lines, int $n, string $key): bool { return in_array($key, array('FieldValue', 'FieldValueDefault')) && diff --git a/src/FdfFile.php b/src/FdfFile.php index 3081bf3..7184614 100644 --- a/src/FdfFile.php +++ b/src/FdfFile.php @@ -41,8 +41,13 @@ class FdfFile extends File * created. Autodetected if not provided. * @param string|null $encoding of the data. Default is 'UTF-8'. */ - public function __construct($data, $suffix = null, $prefix = null, $directory = null, $encoding = 'UTF-8') - { + public function __construct( + array $data, + ?string $suffix = null, + ?string $prefix = null, + ?string $directory = null, + ?string $encoding = 'UTF-8', + ) { if ($directory === null) { $directory = self::getTempDir(); } diff --git a/src/InfoFields.php b/src/InfoFields.php index 9599e14..b152b6b 100644 --- a/src/InfoFields.php +++ b/src/InfoFields.php @@ -10,12 +10,13 @@ * * @author Burak USGURLU * @license http://www.opensource.org/licenses/MIT + * @extends ArrayObject */ class InfoFields extends ArrayObject { - private $_string; + private string $_string; - private $_array; + private array $_array; /** * InfoFields constructor. @@ -24,26 +25,23 @@ class InfoFields extends ArrayObject * @param int $flags * @param string $iterator_class */ - public function __construct($input = null, $flags = 0, $iterator_class = "ArrayIterator") - { + public function __construct( + ?string $input = null, + int $flags = 0, + string $iterator_class = "ArrayIterator", + ) { $this->_string = $input ?: ''; $this->_array = $this->parseData($this->_string); - return parent::__construct($this->_array, $flags, $iterator_class); + parent::__construct($this->_array, $flags, $iterator_class); } - /** - * @return string - */ - public function __toString() + public function __toString(): string { return $this->_string; } - /** - * @return array - */ - public function __toArray() + public function __toArray(): array { return $this->_array; } @@ -70,11 +68,8 @@ public function __toArray() * BookmarkTitle: Second bookmark * BookmarkLevel: 1 * BookmarkPageNumber: 2 - * - * @param $dataString - * @return array */ - private function parseData($dataString) + private function parseData(string $dataString): array { $output = array(); foreach (explode(PHP_EOL, $dataString) as $line) { diff --git a/src/InfoFile.php b/src/InfoFile.php index 7315a6f..61321fd 100644 --- a/src/InfoFile.php +++ b/src/InfoFile.php @@ -3,6 +3,7 @@ namespace mikehaertl\pdftk; use Exception; +use mikehaertl\pdftk\InfoFields; use mikehaertl\tmp\File; /** @@ -70,7 +71,7 @@ class InfoFile extends File * or 'PdfID0' are ignored as those are not part of the PDF's metadata. * All array elements are optional. * @param string|null $suffix the optional suffix for the tmp file - * @param string|null $suffix the optional prefix for the tmp file. If null + * @param string|null $prefix the optional prefix for the tmp file. If null * 'php_tmpfile_' is used. * @param string|null $directory directory where the file should be * created. Autodetected if not provided. @@ -80,8 +81,13 @@ class InfoFile extends File * @throws Exception on invalid data format or if mbstring extension is * missing and data must be converted */ - public function __construct($data, $suffix = null, $prefix = null, $directory = null, $encoding = 'UTF-8') - { + public function __construct( + array|InfoFields $data, + ?string $suffix = null, + ?string $prefix = null, + ?string $directory = null, + ?string $encoding = 'UTF-8', + ) { if ($suffix === null) { $suffix = '.txt'; } @@ -102,7 +108,7 @@ public function __construct($data, $suffix = null, $prefix = null, $directory = } $fields = ''; - $normalizedData = self::normalize($data); + $normalizedData = self::normalize((array) $data); foreach ($normalizedData as $block => $items) { $fields .= self::renderBlock($block, $items, $encoding); @@ -123,7 +129,7 @@ public function __construct($data, $suffix = null, $prefix = null, $directory = * @param array $data the data to normalize * @return array a normalized array in the format described in the constructor */ - private static function normalize($data) + private static function normalize(array $data): array { $normalized = array(); foreach ($data as $key => $value) { @@ -147,7 +153,7 @@ private static function normalize($data) * @param string $encoding the encoding of the item data * @return string the rendered fields */ - private static function renderBlock($block, $items, $encoding) + private static function renderBlock(string $block, array $items, string $encoding): string { $fields = ''; foreach ($items as $key => $value) { @@ -173,8 +179,13 @@ private static function renderBlock($block, $items, $encoding) * @param bool $isInfo whether it's an 'Info' field * @return string the rendered field */ - private static function renderField($prefix, $key, $value, $encoding, $isInfo) - { + private static function renderField( + string $prefix, + string $key, + string $value, + string $encoding, + bool $isInfo, + ): string { if ($encoding !== 'UTF-8') { $value = mb_convert_encoding($value, 'UTF-8', $encoding); $key = mb_convert_encoding($key, 'UTF-8', $encoding); diff --git a/src/Pdf.php b/src/Pdf.php index f7c02f0..937a0b2 100644 --- a/src/Pdf.php +++ b/src/Pdf.php @@ -22,65 +22,65 @@ class Pdf private const TMP_PREFIX = 'tmp_php_pdftk_'; /** - * @var bool whether to ignore any errors if some non-empty output file was + * Whether to ignore any errors if some non-empty output file was * still created. Default is false. */ - public $ignoreWarnings = false; + public bool $ignoreWarnings = false; /** - * @var null|string an optional directory where temporary files should be + * An optional directory where temporary files should be * created. If left empty the directory is autodetected. */ - public $tempDir; + public ?string $tempDir = null; /** - * @var File the temporary output file + * The temporary output file */ - protected $_tmpFile; + protected ?File $_tmpFile = null; /** - * @var string the content type of the tmp output + * The content type of the tmp output */ - protected $_tmpOutputContentType = 'application/pdf'; + protected string $_tmpOutputContentType = 'application/pdf'; /** - * @var Command the command instance that executes pdftk + * The command instance that executes pdftk */ - protected $_command; + protected ?Command $_command = null; /** - * @var int a counter for autogenerated handles + * A counter for autogenerated handles */ - protected $_handle = 0; + protected int $_handle = 0; /** - * @var string the error message + * The error message */ - protected $_error = ''; + protected string $_error = ''; /** - * @var string|null the output filename. If null (default) a tmp file is + * The output filename. If null (default) a tmp file is * used as output. If false, no output option is added at all. */ - protected $_output; + protected ?string $_output = null; /** - * @var string the PDF data as returned from getData() + * The PDF data as returned from getData() */ - protected $_data; - protected $_data_utf8; + protected ?InfoFields $_data = null; + protected ?InfoFields $_data_utf8 = null; /** - * @var DataFields the PDF form field data as returned from getDataFields() + * The PDF form field data as returned from getDataFields() */ - protected $_dataFields; - protected $_dataFields_utf8; + protected ?DataFields $_dataFields = null; + protected ?DataFields $_dataFields_utf8 = null; /** - * @var Pdf[]|null if the input was an instance, we keep a reference here, + * @var Pdf[] if the input was an instance, we keep a reference here, * so that it won't get unlinked before this object gets destroyed */ - protected $_pdfs; + protected array $_pdfs = []; /** * @param string|Pdf|array $pdf a pdf filename or Pdf instance or an array @@ -90,7 +90,7 @@ class Pdf * @param array $options Options to pass to set on the Command instance, * e.g. the pdftk binary path */ - public function __construct($pdf = null, $options = array()) + public function __construct(string|Pdf|array|null $pdf = null, array $options = array()) { $command = $this->getCommand(); if ($options !== array()) { @@ -118,7 +118,7 @@ public function __construct($pdf = null, $options = array()) * @param string|null $password the owner (or user) password if any * @return Pdf the pdf instance for method chaining */ - public function addFile($name, $handle = null, $password = null) + public function addFile(string|Pdf $name, ?string $handle = null, ?string $password = null): self { if ($handle === null || is_numeric($handle)) { $handle = $this->nextHandle(); @@ -173,8 +173,13 @@ public function addFile($name, $handle = null, $password = null) * @param string $rotation the rotation to apply to the pages. * @return Pdf the pdf instance for method chaining */ - public function cat($start = null, $end = null, $handle = null, $qualifier = null, $rotation = null) - { + public function cat( + int|string|array|null $start = null, + int|string|null $end = null, + ?string $handle = null, + ?string $qualifier = null, + ?string $rotation = null, + ): self { $this->getCommand() ->setOperation('cat') ->addPageRange($start, $end, $handle, $qualifier, $rotation); @@ -198,19 +203,24 @@ public function cat($start = null, $end = null, $handle = null, $qualifier = nul * * This will give the page order 1, 4, 3, 5, 2, 9 in the out.pdf * - * @param string $handle the handle of the input file to use - * @param int|array $start the start page number or an array of page + * @param int|string|array $start the start page number or an array of page * numbers. - * @param int|null $end the end page number or null for single page (or + * @param int|string|null $end the end page number or null for single page (or * list if $start is an array) + * @param string $handle the handle of the input file to use * @param string|null $qualifier the page number qualifier, either 'even' * or 'odd' or null for none * @param string $rotation the rotation to apply to the pages. See cat() * for more details. * @return Pdf the pdf instance for method chaining */ - public function shuffle($start, $end = null, $handle = null, $qualifier = null, $rotation = null) - { + public function shuffle( + int|string|array $start, + int|string|null $end = null, + ?string $handle = null, + ?string $qualifier = null, + ?string $rotation = null, + ): self { $this->getCommand() ->setOperation('shuffle') ->addPageRange($start, $end, $handle, $qualifier, $rotation); @@ -224,7 +234,7 @@ public function shuffle($start, $end = null, $handle = null, $qualifier = null, * null for default 'pg_%04d.pdf' * @return bool whether the burst operation was successful */ - public function burst($filepattern = null) + public function burst(?string $filepattern = null): bool { $this->constrainSingleFile(); $this->getCommand()->setOperation('burst'); @@ -238,9 +248,10 @@ public function burst($filepattern = null) * @param array $files the list of full paths to the files to attach * @param string $toPage the page to add the attachment to. If omitted the * files are attached at the document level. - * @return bool whether the operation was successful + * + * @return self whether the operation was successful */ - public function attachFiles($files, $toPage = null) + public function attachFiles(array $files, ?string $toPage = null): self { $this->constrainSingleFile(); if ($toPage !== null) { @@ -259,7 +270,7 @@ public function attachFiles($files, $toPage = null) * @param string|null $dir the output directory * @return bool whether the operation was successful */ - public function unpackFiles($dir = null) + public function unpackFiles(?string $dir = null): bool { $this->constrainSingleFile(); $this->getCommand()->setOperation('unpack_files'); @@ -273,7 +284,7 @@ public function unpackFiles($dir = null) * @param string $name name of the FDF file * @return bool whether the pdf is generated successful */ - public function generateFdfFile($name) + public function generateFdfFile(string $name): bool { $this->constrainSingleFile(); $this->getCommand()->setOperation('generate_fdf'); @@ -294,8 +305,12 @@ public function generateFdfFile($name) * give best results so you should not have to change the default. * @return Pdf the pdf instance for method chaining */ - public function fillForm($data, $encoding = 'UTF-8', $dropXfa = true, $format = 'xfdf') - { + public function fillForm( + string|array $data, + string $encoding = 'UTF-8', + bool $dropXfa = true, + string $format = 'xfdf', + ): self { $this->constrainSingleFile(); if (is_array($data)) { $className = '\mikehaertl\pdftk\\' . ($format === 'xfdf' ? 'XfdfFile' : 'FdfFile'); @@ -319,7 +334,7 @@ public function fillForm($data, $encoding = 'UTF-8', $dropXfa = true, $format = * @param string the encoding of the data. Default is 'UTF-8'. * @return Pdf the pdf instance for method chaining */ - public function updateInfo($data, $encoding = 'UTF-8') + public function updateInfo(string|array|InfoFields|InfoFile $data, string $encoding = 'UTF-8'): self { $this->constrainSingleFile(); if (is_array($data) || $data instanceof InfoFields) { @@ -342,7 +357,7 @@ public function updateInfo($data, $encoding = 'UTF-8') * is used. * @return Pdf the pdf instance for method chaining */ - public function background($file) + public function background(string $file): self { $this->constrainSingleFile(); $this->getCommand() @@ -361,7 +376,7 @@ public function background($file) * @param string $file name of the background PDF file. * @return Pdf the pdf instance for method chaining */ - public function multiBackground($file) + public function multiBackground(string $file): self { $this->getCommand() ->setOperation('multibackground') @@ -378,7 +393,7 @@ public function multiBackground($file) * first page is used. * @return Pdf the pdf instance for method chaining */ - public function stamp($file) + public function stamp(string $file): self { $this->constrainSingleFile(); $this->getCommand() @@ -397,7 +412,7 @@ public function stamp($file) * @param string $file name of the PDF file to add as overlay * @return Pdf the pdf instance for method chaining */ - public function multiStamp($file) + public function multiStamp(string $file): self { $this->getCommand() ->setOperation('multistamp') @@ -410,7 +425,7 @@ public function multiStamp($file) * true. * @return InfoFields|bool meta data about the PDF or false on failure */ - public function getData($utf8 = true) + public function getData(bool $utf8 = true): InfoFields|bool { $property = $utf8 ? '_data_utf8' : '_data'; if ($this->$property === null) { @@ -432,7 +447,7 @@ public function getData($utf8 = true) * @return DataFields|bool data about the PDF form fields or false on * failure */ - public function getDataFields($utf8 = true) + public function getDataFields(bool $utf8 = true): DataFields|bool { $property = $utf8 ? '_dataFields_utf8' : '_dataFields'; if ($this->$property === null) { @@ -458,7 +473,7 @@ public function getDataFields($utf8 = true) * ModifyAnnotations, FillIn, AllFeatures. * @return Pdf the pdf instance for method chaining */ - public function allow($permissions = null) + public function allow($permissions = null): self { $this->getCommand() ->addOption('allow', $permissions, false); @@ -470,7 +485,7 @@ public function allow($permissions = null) * * @return Pdf the pdf instance for method chaining */ - public function flatten() + public function flatten(): self { $this->getCommand() ->addOption('flatten'); @@ -484,7 +499,7 @@ public function flatten() * compression * @return Pdf the pdf instance for method chaining */ - public function compress($compress = true) + public function compress($compress = true): self { $this->getCommand() ->addOption($compress ? 'compress' : 'uncompress'); @@ -498,7 +513,7 @@ public function compress($compress = true) * @param string $id, either 'first' (default) or 'last' * @return Pdf the pdf instance for method chaining */ - public function keepId($id = 'first') + public function keepId($id = 'first'): self { $this->getCommand() ->addOption($id === 'first' ? 'keep_first_id' : 'keep_final_id'); @@ -515,7 +530,7 @@ public function keepId($id = 'first') * * @return Pdf the pdf instance for method chaining */ - public function needAppearances() + public function needAppearances(): self { $this->getCommand() ->addOption('need_appearances'); @@ -532,7 +547,7 @@ public function needAppearances() * * @return Pdf the pdf instance for method chaining */ - public function dropXfa() + public function dropXfa(): self { $this->getCommand() ->addOption('drop_xfa'); @@ -549,7 +564,7 @@ public function dropXfa() * * @return Pdf the pdf instance for method chaining */ - public function dropXmp() + public function dropXmp(): self { $this->getCommand() ->addOption('drop_xmp'); @@ -560,7 +575,7 @@ public function dropXmp() * @param string $password the owner password to set on the output PDF * @return Pdf the pdf instance for method chaining */ - public function setPassword($password) + public function setPassword(string $password): self { $this->getCommand() ->addOption('owner_pw', $password, true); @@ -571,7 +586,7 @@ public function setPassword($password) * @param string $password the user password to set on the output PDF * @return Pdf the pdf instance for method chaining */ - public function setUserPassword($password) + public function setUserPassword(string $password): self { $this->getCommand() ->addOption('user_pw', $password, true); @@ -599,7 +614,7 @@ public function passwordEncryption($strength = 128) * @param string $fontName the path to the font or the name of a font family. * @return Pdf the pdf instance for method chaining */ - public function replacementFont($path) + public function replacementFont(string $path): self { $this->getCommand() ->addOption('replacement_font', $path); @@ -612,7 +627,7 @@ public function replacementFont($path) * @param string $name of output file * @return bool whether the PDF could be processed and saved */ - public function saveAs($name) + public function saveAs(string $name): bool { if (!$this->getCommand()->getExecuted() && !$this->execute()) { return false; @@ -641,7 +656,7 @@ public function saveAs($name) * automatically created headers, `false` can also be used as header value. * @return bool whether PDF was created successfully */ - public function send($filename = null, $inline = false, $headers = array()) + public function send(?string $filename = null, bool $inline = false, array $headers = array()): bool { if (!$this->getCommand()->getExecuted() && !$this->execute()) { return false; @@ -656,7 +671,7 @@ public function send($filename = null, $inline = false, $headers = array()) * @return string|bool the PDF content as a string or `false` if the PDF * wasn't created successfully. */ - public function toString() + public function toString(): string|bool { if (!$this->getCommand()->getExecuted() && !$this->execute()) { return false; @@ -667,7 +682,7 @@ public function toString() /** * @return Command the command instance that executes pdftk */ - public function getCommand() + public function getCommand(): Command { if ($this->_command === null) { $this->_command = new Command(); @@ -678,7 +693,7 @@ public function getCommand() /** * @return File the temporary output file instance */ - public function getTmpFile() + public function getTmpFile(): File { if ($this->_tmpFile === null) { $this->_tmpFile = new File('', '.pdf', self::TMP_PREFIX, $this->tempDir); @@ -689,7 +704,7 @@ public function getTmpFile() /** * @return string the error message or an empty string if none */ - public function getError() + public function getError(): string { return $this->_error; } @@ -701,7 +716,7 @@ public function getError() * * @return bool whether the command was executed successfully */ - public function execute() + public function execute(): bool { $command = $this->getCommand(); if ($command->getExecuted()) { @@ -725,7 +740,7 @@ public function execute() /** * Make sure, that only one file is present */ - protected function constrainSingleFile() + protected function constrainSingleFile(): void { if ($this->getCommand()->getFileCount() > 1) { throw new \Exception('This operation can only process single files'); @@ -735,7 +750,7 @@ protected function constrainSingleFile() /** * @return string the next handle in the series A, B, C, ... Z, AA, AB... */ - protected function nextHandle() + protected function nextHandle(): string { // N.B. Multi-character handles are only available in pdftk 1.45+ diff --git a/src/XfdfFile.php b/src/XfdfFile.php index ff75a16..af99556 100644 --- a/src/XfdfFile.php +++ b/src/XfdfFile.php @@ -54,18 +54,18 @@ class XfdfFile extends File { // XFDF file header private const XFDF_HEADER = << - - + + + -FDF; + FDF; // XFDF file footer private const XFDF_FOOTER = << - + + -FDF; + FDF; /** * Constructor @@ -79,8 +79,13 @@ class XfdfFile extends File * created. Autodetected if not provided. * @param string|null $encoding of the data. Default is 'UTF-8'. */ - public function __construct($data, $suffix = null, $prefix = null, $directory = null, $encoding = 'UTF-8') - { + public function __construct( + array $data, + ?string $suffix = null, + ?string $prefix = null, + ?string $directory = null, + ?string $encoding = 'UTF-8', + ) { if ($directory === null) { $directory = self::getTempDir(); } @@ -91,9 +96,7 @@ public function __construct($data, $suffix = null, $prefix = null, $directory = $prefix = 'php_pdftk_xfdf_'; } - $tempfile = tempnam($directory, $prefix); - $this->_fileName = $tempfile . $suffix; - rename($tempfile, $this->_fileName); + parent::__construct('', $suffix, $prefix, $directory); $fields = $this->parseData($data, $encoding); $this->writeXml($fields); @@ -142,23 +145,27 @@ public function __construct($data, $suffix = null, $prefix = null, $directory = * ], * ] * - * * @param mixed $data the data to parse * @param string the encoding of the data + * @param null|string $encoding + * * @return array the result array in UTF-8 encoding with dot keys converted * to nested arrays */ - protected function parseData($data, $encoding) + protected function parseData($data, ?string $encoding): array { + $willConvert = $encoding !== 'UTF-8' && function_exists('mb_convert_encoding'); $result = array(); foreach ($data as $key => $value) { - if ($encoding !== 'UTF-8' && function_exists('mb_convert_encoding')) { + $key = (string) $key; + if ($willConvert) { $key = mb_convert_encoding($key, 'UTF-8', $encoding); $value = mb_convert_encoding($value, 'UTF-8', $encoding); } if (strpos($key, '.') === false) { $result['_' . $key] = $value; } else { + /** @var array $target */ $target = &$result; $keyParts = explode('.', $key); $lastPart = array_pop($keyParts); @@ -166,6 +173,7 @@ protected function parseData($data, $encoding) if (!isset($target['_' . $part])) { $target['_' . $part] = array(); } + /** @var array $target */ $target = &$target['_' . $part]; } $target['_' . $lastPart] = $value; @@ -179,7 +187,7 @@ protected function parseData($data, $encoding) * * @param array $fields the fields in a nested array structure */ - protected function writeXml($fields) + protected function writeXml(array $fields): void { // Use fwrite, since file_put_contents() messes around with character encoding $fp = fopen($this->_fileName, 'w'); @@ -192,11 +200,11 @@ protected function writeXml($fields) /** * Write the fields to the given filepointer * - * @param int $fp + * @param resource $fp * @param mixed[] $fields an array of field values as returned by * `parseData()`. */ - protected function writeFields($fp, $fields) + protected function writeFields($fp, array $fields): void { foreach ($fields as $key => $value) { $key = $this->xmlEncode(substr($key, 1)); @@ -222,7 +230,7 @@ protected function writeFields($fp, $fields) * @param string|null $value the value to encode * @return string|null the value correctly encoded for use in a XML document */ - protected function xmlEncode($value) + protected function xmlEncode(?string $value): ?string { if ($value === null) { return null;