功能合并:插件更新

This commit is contained in:
wuhui_zzw 2023-12-13 16:55:55 +08:00
parent ff1fa5d79b
commit 406c03a8a1
690 changed files with 61917 additions and 1390 deletions

2
vendor/endroid/qr-code/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/.github/workflows/ export-ignore
/tests/ export-ignore

View File

@ -11,79 +11,128 @@
This library helps you generate QR codes in a jiffy. Makes use of [bacon/bacon-qr-code](https://github.com/Bacon/BaconQrCode) This library helps you generate QR codes in a jiffy. Makes use of [bacon/bacon-qr-code](https://github.com/Bacon/BaconQrCode)
to generate the matrix and [khanamiryan/qrcode-detector-decoder](https://github.com/khanamiryan/php-qrcode-detector-decoder) to generate the matrix and [khanamiryan/qrcode-detector-decoder](https://github.com/khanamiryan/php-qrcode-detector-decoder)
for validating generated QR codes. Further extended with Twig extensions, generation routes, a factory and a for validating generated QR codes. Further extended with Twig extensions, generation routes, a factory and a
Symfony bundle for easy installation and configuration. Symfony bundle for easy installation and configuration. Different writers are provided to generate the QR code
as PNG, SVG, EPS or in binary format.
Different writers are provided to generate the QR code as PNG, SVG, EPS or in binary format.
## Installation ## Installation
Use [Composer](https://getcomposer.org/) to install the library. Use [Composer](https://getcomposer.org/) to install the library. Also make sure you have enabled and configured the
[GD extension](https://www.php.net/manual/en/book.image.php) if you want to generate images.
``` bash ``` bash
$ composer require endroid/qr-code $ composer require endroid/qr-code
``` ```
## Basic usage ## Usage: using the builder
```php ```php
use Endroid\QrCode\QrCode; use Endroid\QrCode\Builder\Builder;
use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\Label\Alignment\LabelAlignmentCenter;
use Endroid\QrCode\Label\Font\NotoSans;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter;
$qrCode = new QrCode('Life is too short to be generating QR codes'); $result = Builder::create()
->writer(new PngWriter())
header('Content-Type: '.$qrCode->getContentType()); ->writerOptions([])
echo $qrCode->writeString(); ->data('Custom QR code contents')
->encoding(new Encoding('UTF-8'))
->errorCorrectionLevel(new ErrorCorrectionLevelHigh())
->size(300)
->margin(10)
->roundBlockSizeMode(new RoundBlockSizeModeMargin())
->logoPath(__DIR__.'/assets/symfony.png')
->labelText('This is the label')
->labelFont(new NotoSans(20))
->labelAlignment(new LabelAlignmentCenter())
->build();
``` ```
## Advanced usage ## Usage: without using the builder
```php ```php
use Endroid\QrCode\ErrorCorrectionLevel; use Endroid\QrCode\Color\Color;
use Endroid\QrCode\LabelAlignment; use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\QrCode; use Endroid\QrCode\QrCode;
use Endroid\QrCode\Response\QrCodeResponse; use Endroid\QrCode\Label\Label;
use Endroid\QrCode\Logo\Logo;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\Writer\PngWriter;
// Create a basic QR code $writer = new PngWriter();
$qrCode = new QrCode('Life is too short to be generating QR codes');
$qrCode->setSize(300);
// Set advanced options // Create QR code
$qrCode->setWriterByName('png'); $qrCode = QrCode::create('Data')
$qrCode->setEncoding('UTF-8'); ->setEncoding(new Encoding('UTF-8'))
$qrCode->setErrorCorrectionLevel(ErrorCorrectionLevel::HIGH()); ->setErrorCorrectionLevel(new ErrorCorrectionLevelLow())
$qrCode->setForegroundColor(['r' => 0, 'g' => 0, 'b' => 0, 'a' => 0]); ->setSize(300)
$qrCode->setBackgroundColor(['r' => 255, 'g' => 255, 'b' => 255, 'a' => 0]); ->setMargin(10)
$qrCode->setLabel('Scan the code', 16, __DIR__.'/../assets/fonts/noto_sans.otf', LabelAlignment::CENTER()); ->setRoundBlockSizeMode(new RoundBlockSizeModeMargin())
$qrCode->setLogoPath(__DIR__.'/../assets/images/symfony.png'); ->setForegroundColor(new Color(0, 0, 0))
$qrCode->setLogoSize(150, 200); ->setBackgroundColor(new Color(255, 255, 255));
$qrCode->setValidateResult(false);
// Apply a margin and round block sizes to improve readability // Create generic logo
// Please note that rounding block sizes can result in additional margin $logo = Logo::create(__DIR__.'/assets/symfony.png')
$qrCode->setRoundBlockSize(true); ->setResizeToWidth(50);
$qrCode->setMargin(10);
// Set additional writer options (SvgWriter example) // Create generic label
$qrCode->setWriterOptions(['exclude_xml_declaration' => true]); $label = Label::create('Label')
->setTextColor(new Color(255, 0, 0));
$result = $writer->write($qrCode, $logo, $label);
```
## Usage: working with results
```php
// Directly output the QR code // Directly output the QR code
header('Content-Type: '.$qrCode->getContentType()); header('Content-Type: '.$result->getMimeType());
echo $qrCode->writeString(); echo $result->getString();
// Save it to a file // Save it to a file
$qrCode->writeFile(__DIR__.'/qrcode.png'); $result->saveToFile(__DIR__.'/qrcode.png');
// Generate a data URI to include image data inline (i.e. inside an <img> tag) // Generate a data URI to include image data inline (i.e. inside an <img> tag)
$dataUri = $qrCode->writeDataUri(); $dataUri = $result->getDataUri();
``` ```
![QR Code](https://endroid.nl/qr-code/Life%20is%20too%20short%20to%20be%20generating%20QR%20codes.png) ![QR Code](https://endroid.nl/qr-code/default/Life%20is%20too%20short%20to%20be%20generating%20QR%20codes)
### Writer options
```php
use Endroid\QrCode\Writer\SvgWriter;
$builder->setWriterOptions([SvgWriter::WRITER_OPTION_EXCLUDE_XML_DECLARATION => true]);
```
### Encoding ### Encoding
You can pick one of these values for encoding:
`ISO-8859-1`, `ISO-8859-2`, `ISO-8859-3`, `ISO-8859-4`, `ISO-8859-5`, `ISO-8859-6`, `ISO-8859-7`, `ISO-8859-8`, `ISO-8859-9`, `ISO-8859-10`, `ISO-8859-11`, `ISO-8859-12`, `ISO-8859-13`, `ISO-8859-14`, `ISO-8859-15`, `ISO-8859-16`, `Shift_JIS`, `windows-1250`, `windows-1251`, `windows-1252`, `windows-1256`, `UTF-16BE`, `UTF-8`, `US-ASCII`, `GBK` `EUC-KR` If you use a barcode scanner you can have some troubles while reading the
generated QR codes. Depending on the encoding you chose you will have an extra
amount of data corresponding to the ECI block. Some barcode scanner are not
programmed to interpret this block of information. To ensure a maximum
compatibility you can use the `ISO-8859-1` encoding that is the default
encoding used by barcode scanners (if your character set supports it,
i.e. no Chinese characters are present).
If you use a barcode scanner you can have some troubles while reading the generated QR codes. Depending on the encoding you chose you will have an extra amount of data corresponding to the ECI block. Some barcode scanner are not programmed to interpret this block of information. For exemple the ECI block for `UTF-8` is `000026` so the above exemple will produce : `\000026Life is too short to be generating QR codes`. To ensure a maximum compatibility you can use the `ISO-8859-1` encoding that is the default encoding used by barcode scanners. ### Round block size mode
By default block sizes are rounded to guarantee sharp images and improve
readability. However some other rounding variants are available.
* `margin (default)`: the size of the QR code is shrunk if necessary but the size
of the final image remains unchanged due to additional margin being added.
* `enlarge`: the size of the QR code and the final image are enlarged when
rounding differences occur.
* `shrink`: the size of the QR code and the final image are
shrunk when rounding differences occur.
* `none`: No rounding. This mode can be used when blocks don't need to be rounded
to pixels (for instance SVG).
## Readability ## Readability
@ -93,15 +142,16 @@ can tweak these parameters if you are looking for optimal results. You can also
check $qrCode->getRoundBlockSize() value to see if block dimensions are rounded check $qrCode->getRoundBlockSize() value to see if block dimensions are rounded
so that the image is more sharp and readable. Please note that rounding block so that the image is more sharp and readable. Please note that rounding block
size can result in additional padding to compensate for the rounding difference. size can result in additional padding to compensate for the rounding difference.
And finally the encoding (default UTF-8 to support large character sets) can be
set to `ISO-8859-1` if possible to improve readability.
## Built-in validation reader ## Built-in validation reader
You can enable the built-in validation reader (disabled by default) by calling You can enable the built-in validation reader (disabled by default) by calling
setValidateResult(true). This validation reader does not guarantee that the QR setValidateResult(true). This validation reader does not guarantee that the QR
code will be readable by all readers but it helps you provide a minimum level code will be readable by all readers but it helps you provide a minimum level
of quality. of quality. Take note that the validator can consume quite amount of additional
resources and it should be installed separately only if you use it.
Take note that the validator can consume quite amount of additional resources.
## Symfony integration ## Symfony integration
@ -109,8 +159,8 @@ The [endroid/qr-code-bundle](https://github.com/endroid/qr-code-bundle)
integrates the QR code library in Symfony for an even better experience. integrates the QR code library in Symfony for an even better experience.
* Configure your defaults (like image size, default writer etc.) * Configure your defaults (like image size, default writer etc.)
* Generate QR codes quickly from anywhere via the factory service * Support for multiple configurations and injection via aliases
* Generate QR codes directly by typing an URL like /qr-code/\<text>.png?size=300 * Generate QR codes for defined configurations via URL like /qr-code/<config>/Hello
* Generate QR codes or URLs directly from Twig using dedicated functions * Generate QR codes or URLs directly from Twig using dedicated functions
Read the [bundle documentation](https://github.com/endroid/qr-code-bundle) Read the [bundle documentation](https://github.com/endroid/qr-code-bundle)

Binary file not shown.

Binary file not shown.

View File

@ -1,7 +1,7 @@
{ {
"name": "endroid/qr-code", "name": "endroid/qr-code",
"description": "Endroid QR Code", "description": "Endroid QR Code",
"keywords": ["endroid", "qrcode", "qr", "code", "bundle", "php"], "keywords": ["endroid", "qrcode", "qr", "code", "php"],
"homepage": "https://github.com/endroid/qr-code", "homepage": "https://github.com/endroid/qr-code",
"type": "library", "type": "library",
"license": "MIT", "license": "MIT",
@ -12,20 +12,20 @@
} }
], ],
"require": { "require": {
"php": ">=7.2", "php": "^7.4||^8.0",
"ext-gd": "*", "bacon/bacon-qr-code": "^2.0.5"
"bacon/bacon-qr-code": "^2.0",
"khanamiryan/qrcode-detector-decoder": "^1.0.2",
"symfony/options-resolver": "^3.4||^4.4||^5.0",
"symfony/property-access": "^3.4||^4.4||^5.0",
"myclabs/php-enum": "^1.5"
}, },
"require-dev": { "require-dev": {
"endroid/quality": "dev-master" "ext-gd": "*",
"endroid/quality": "dev-master",
"khanamiryan/qrcode-detector-decoder": "^1.0.4",
"setasign/fpdf": "^1.8.2"
}, },
"suggest": { "suggest": {
"roave/security-advisories": "Avoids installation of package versions with vulnerabilities", "ext-gd": "Enables you to write PNG images",
"symfony/security-checker": "Checks your composer.lock for vulnerabilities" "khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator",
"roave/security-advisories": "Makes sure package versions with known security issues are not installed",
"setasign/fpdf": "Enables you to use the PDF writer"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@ -38,11 +38,14 @@
} }
}, },
"config": { "config": {
"sort-packages": true "sort-packages": true,
"preferred-install": {
"endroid/*": "source"
}
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.x-dev" "dev-master": "4.x-dev"
} }
} }
} }

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Bacon;
use BaconQrCode\Common\ErrorCorrectionLevel;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelMedium;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelQuartile;
final class ErrorCorrectionLevelConverter
{
public static function convertToBaconErrorCorrectionLevel(ErrorCorrectionLevelInterface $errorCorrectionLevel): ErrorCorrectionLevel
{
if ($errorCorrectionLevel instanceof ErrorCorrectionLevelLow) {
return ErrorCorrectionLevel::valueOf('L');
} elseif ($errorCorrectionLevel instanceof ErrorCorrectionLevelMedium) {
return ErrorCorrectionLevel::valueOf('M');
} elseif ($errorCorrectionLevel instanceof ErrorCorrectionLevelQuartile) {
return ErrorCorrectionLevel::valueOf('Q');
} elseif ($errorCorrectionLevel instanceof ErrorCorrectionLevelHigh) {
return ErrorCorrectionLevel::valueOf('H');
}
throw new \Exception('Error correction level could not be converted');
}
}

View File

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Bacon;
use BaconQrCode\Encoder\Encoder;
use Endroid\QrCode\Matrix\Matrix;
use Endroid\QrCode\Matrix\MatrixFactoryInterface;
use Endroid\QrCode\Matrix\MatrixInterface;
use Endroid\QrCode\QrCodeInterface;
final class MatrixFactory implements MatrixFactoryInterface
{
public function create(QrCodeInterface $qrCode): MatrixInterface
{
$baconErrorCorrectionLevel = ErrorCorrectionLevelConverter::convertToBaconErrorCorrectionLevel($qrCode->getErrorCorrectionLevel());
$baconMatrix = Encoder::encode($qrCode->getData(), $baconErrorCorrectionLevel, strval($qrCode->getEncoding()))->getMatrix();
$blockValues = [];
$columnCount = $baconMatrix->getWidth();
$rowCount = $baconMatrix->getHeight();
for ($rowIndex = 0; $rowIndex < $rowCount; ++$rowIndex) {
$blockValues[$rowIndex] = [];
for ($columnIndex = 0; $columnIndex < $columnCount; ++$columnIndex) {
$blockValues[$rowIndex][$columnIndex] = $baconMatrix->get($columnIndex, $rowIndex);
}
}
return new Matrix($blockValues, $qrCode->getSize(), $qrCode->getMargin(), $qrCode->getRoundBlockSizeMode());
}
}

View File

@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Builder;
use Endroid\QrCode\Color\ColorInterface;
use Endroid\QrCode\Encoding\EncodingInterface;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
use Endroid\QrCode\Label\Alignment\LabelAlignmentInterface;
use Endroid\QrCode\Label\Font\FontInterface;
use Endroid\QrCode\Label\Label;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Label\Margin\MarginInterface;
use Endroid\QrCode\Logo\Logo;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCode;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeInterface;
use Endroid\QrCode\Writer\PngWriter;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Endroid\QrCode\Writer\ValidatingWriterInterface;
use Endroid\QrCode\Writer\WriterInterface;
class Builder implements BuilderInterface
{
/**
* @var array<mixed>{
* data: string,
* writer: WriterInterface,
* writerOptions: array,
* qrCodeClass: class-string,
* logoClass: class-string,
* labelClass: class-string,
* validateResult: bool,
* size?: int,
* encoding?: EncodingInterface,
* errorCorrectionLevel?: ErrorCorrectionLevelInterface,
* roundBlockSizeMode?: RoundBlockSizeModeInterface,
* margin?: int,
* backgroundColor?: ColorInterface,
* foregroundColor?: ColorInterface,
* labelText?: string,
* labelFont?: FontInterface,
* labelAlignment?: LabelAlignmentInterface,
* labelMargin?: MarginInterface,
* labelTextColor?: ColorInterface,
* logoPath?: string,
* logoResizeToWidth?: int,
* logoResizeToHeight?: int,
* logoPunchoutBackground?: bool
* }
*/
private array $options;
public function __construct()
{
$this->options = [
'data' => '',
'writer' => new PngWriter(),
'writerOptions' => [],
'qrCodeClass' => QrCode::class,
'logoClass' => Logo::class,
'labelClass' => Label::class,
'validateResult' => false,
];
}
public static function create(): BuilderInterface
{
return new self();
}
public function writer(WriterInterface $writer): BuilderInterface
{
$this->options['writer'] = $writer;
return $this;
}
/** @param array<mixed> $writerOptions */
public function writerOptions(array $writerOptions): BuilderInterface
{
$this->options['writerOptions'] = $writerOptions;
return $this;
}
public function data(string $data): BuilderInterface
{
$this->options['data'] = $data;
return $this;
}
public function encoding(EncodingInterface $encoding): BuilderInterface
{
$this->options['encoding'] = $encoding;
return $this;
}
public function errorCorrectionLevel(ErrorCorrectionLevelInterface $errorCorrectionLevel): BuilderInterface
{
$this->options['errorCorrectionLevel'] = $errorCorrectionLevel;
return $this;
}
public function size(int $size): BuilderInterface
{
$this->options['size'] = $size;
return $this;
}
public function margin(int $margin): BuilderInterface
{
$this->options['margin'] = $margin;
return $this;
}
public function roundBlockSizeMode(RoundBlockSizeModeInterface $roundBlockSizeMode): BuilderInterface
{
$this->options['roundBlockSizeMode'] = $roundBlockSizeMode;
return $this;
}
public function foregroundColor(ColorInterface $foregroundColor): BuilderInterface
{
$this->options['foregroundColor'] = $foregroundColor;
return $this;
}
public function backgroundColor(ColorInterface $backgroundColor): BuilderInterface
{
$this->options['backgroundColor'] = $backgroundColor;
return $this;
}
public function logoPath(string $logoPath): BuilderInterface
{
$this->options['logoPath'] = $logoPath;
return $this;
}
public function logoResizeToWidth(int $logoResizeToWidth): BuilderInterface
{
$this->options['logoResizeToWidth'] = $logoResizeToWidth;
return $this;
}
public function logoResizeToHeight(int $logoResizeToHeight): BuilderInterface
{
$this->options['logoResizeToHeight'] = $logoResizeToHeight;
return $this;
}
public function logoPunchoutBackground(bool $logoPunchoutBackground): BuilderInterface
{
$this->options['logoPunchoutBackground'] = $logoPunchoutBackground;
return $this;
}
public function labelText(string $labelText): BuilderInterface
{
$this->options['labelText'] = $labelText;
return $this;
}
public function labelFont(FontInterface $labelFont): BuilderInterface
{
$this->options['labelFont'] = $labelFont;
return $this;
}
public function labelAlignment(LabelAlignmentInterface $labelAlignment): BuilderInterface
{
$this->options['labelAlignment'] = $labelAlignment;
return $this;
}
public function labelMargin(MarginInterface $labelMargin): BuilderInterface
{
$this->options['labelMargin'] = $labelMargin;
return $this;
}
public function labelTextColor(ColorInterface $labelTextColor): BuilderInterface
{
$this->options['labelTextColor'] = $labelTextColor;
return $this;
}
public function validateResult(bool $validateResult): BuilderInterface
{
$this->options['validateResult'] = $validateResult;
return $this;
}
public function build(): ResultInterface
{
$writer = $this->options['writer'];
if ($this->options['validateResult'] && !$writer instanceof ValidatingWriterInterface) {
throw new \Exception('Unable to validate result with '.get_class($writer));
}
/** @var QrCode $qrCode */
$qrCode = $this->buildObject($this->options['qrCodeClass']);
/** @var LogoInterface|null $logo */
$logo = $this->buildObject($this->options['logoClass'], 'logo');
/** @var LabelInterface|null $label */
$label = $this->buildObject($this->options['labelClass'], 'label');
$result = $writer->write($qrCode, $logo, $label, $this->options['writerOptions']);
if ($this->options['validateResult'] && $writer instanceof ValidatingWriterInterface) {
$writer->validateResult($result, $qrCode->getData());
}
return $result;
}
/**
* @param class-string $class
*
* @return mixed
*/
private function buildObject(string $class, string $optionsPrefix = null)
{
/** @var \ReflectionClass<object> $reflectionClass */
$reflectionClass = new \ReflectionClass($class);
$arguments = [];
$hasBuilderOptions = false;
$missingRequiredArguments = [];
/** @var \ReflectionMethod $constructor */
$constructor = $reflectionClass->getConstructor();
$constructorParameters = $constructor->getParameters();
foreach ($constructorParameters as $parameter) {
$optionName = null === $optionsPrefix ? $parameter->getName() : $optionsPrefix.ucfirst($parameter->getName());
if (isset($this->options[$optionName])) {
$hasBuilderOptions = true;
$arguments[] = $this->options[$optionName];
} elseif ($parameter->isDefaultValueAvailable()) {
$arguments[] = $parameter->getDefaultValue();
} else {
$missingRequiredArguments[] = $optionName;
}
}
if (!$hasBuilderOptions) {
return null;
}
if (count($missingRequiredArguments) > 0) {
throw new \Exception(sprintf('Missing required arguments: %s', implode(', ', $missingRequiredArguments)));
}
return $reflectionClass->newInstanceArgs($arguments);
}
}

View File

@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Builder;
use Endroid\QrCode\Color\ColorInterface;
use Endroid\QrCode\Encoding\EncodingInterface;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
use Endroid\QrCode\Label\Alignment\LabelAlignmentInterface;
use Endroid\QrCode\Label\Font\FontInterface;
use Endroid\QrCode\Label\Margin\MarginInterface;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeInterface;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Endroid\QrCode\Writer\WriterInterface;
interface BuilderInterface
{
public static function create(): BuilderInterface;
public function writer(WriterInterface $writer): BuilderInterface;
/** @param array<mixed> $writerOptions */
public function writerOptions(array $writerOptions): BuilderInterface;
public function data(string $data): BuilderInterface;
public function encoding(EncodingInterface $encoding): BuilderInterface;
public function errorCorrectionLevel(ErrorCorrectionLevelInterface $errorCorrectionLevel): BuilderInterface;
public function size(int $size): BuilderInterface;
public function margin(int $margin): BuilderInterface;
public function roundBlockSizeMode(RoundBlockSizeModeInterface $roundBlockSizeMode): BuilderInterface;
public function foregroundColor(ColorInterface $foregroundColor): BuilderInterface;
public function backgroundColor(ColorInterface $backgroundColor): BuilderInterface;
public function logoPath(string $logoPath): BuilderInterface;
public function logoResizeToWidth(int $logoResizeToWidth): BuilderInterface;
public function logoResizeToHeight(int $logoResizeToHeight): BuilderInterface;
public function logoPunchoutBackground(bool $logoPunchoutBackground): BuilderInterface;
public function labelText(string $labelText): BuilderInterface;
public function labelFont(FontInterface $labelFont): BuilderInterface;
public function labelAlignment(LabelAlignmentInterface $labelAlignment): BuilderInterface;
public function labelMargin(MarginInterface $labelMargin): BuilderInterface;
public function labelTextColor(ColorInterface $labelTextColor): BuilderInterface;
public function validateResult(bool $validateResult): BuilderInterface;
public function build(): ResultInterface;
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Builder;
final class BuilderRegistry implements BuilderRegistryInterface
{
/** @var array<BuilderInterface> */
private array $builders = [];
public function getBuilder(string $name): BuilderInterface
{
if (!isset($this->builders[$name])) {
throw new \Exception(sprintf('Builder with name "%s" not available from registry', $name));
}
return $this->builders[$name];
}
public function addBuilder(string $name, BuilderInterface $builder): void
{
$this->builders[$name] = $builder;
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Builder;
interface BuilderRegistryInterface
{
public function getBuilder(string $name): BuilderInterface;
public function addBuilder(string $name, BuilderInterface $builder): void;
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Color;
final class Color implements ColorInterface
{
private int $red;
private int $green;
private int $blue;
private int $alpha;
public function __construct(int $red, int $green, int $blue, int $alpha = 0)
{
$this->red = $red;
$this->green = $green;
$this->blue = $blue;
$this->alpha = $alpha;
}
public function getRed(): int
{
return $this->red;
}
public function getGreen(): int
{
return $this->green;
}
public function getBlue(): int
{
return $this->blue;
}
public function getAlpha(): int
{
return $this->alpha;
}
public function getOpacity(): float
{
return 1 - $this->alpha / 127;
}
public function toArray(): array
{
return [
'red' => $this->red,
'green' => $this->green,
'blue' => $this->blue,
'alpha' => $this->alpha,
];
}
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Color;
interface ColorInterface
{
public function getRed(): int;
public function getGreen(): int;
public function getBlue(): int;
public function getAlpha(): int;
public function getOpacity(): float;
/** @return array<string, int> */
public function toArray(): array;
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Encoding;
final class Encoding implements EncodingInterface
{
private string $value;
public function __construct(string $value)
{
if (!in_array($value, mb_list_encodings())) {
throw new \Exception(sprintf('Invalid encoding "%s"', $value));
}
$this->value = $value;
}
public function __toString(): string
{
return $this->value;
}
}

View File

@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Encoding;
interface EncodingInterface
{
public function __toString(): string;
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\ErrorCorrectionLevel;
final class ErrorCorrectionLevelHigh implements ErrorCorrectionLevelInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\ErrorCorrectionLevel;
interface ErrorCorrectionLevelInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\ErrorCorrectionLevel;
final class ErrorCorrectionLevelLow implements ErrorCorrectionLevelInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\ErrorCorrectionLevel;
final class ErrorCorrectionLevelMedium implements ErrorCorrectionLevelInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\ErrorCorrectionLevel;
final class ErrorCorrectionLevelQuartile implements ErrorCorrectionLevelInterface
{
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\ImageData;
use Endroid\QrCode\Label\LabelInterface;
class LabelImageData
{
private int $width;
private int $height;
private function __construct(int $width, int $height)
{
$this->width = $width;
$this->height = $height;
}
public static function createForLabel(LabelInterface $label): self
{
if (false !== strpos($label->getText(), "\n")) {
throw new \Exception('Label does not support line breaks');
}
if (!function_exists('imagettfbbox')) {
throw new \Exception('Function "imagettfbbox" does not exist: check your FreeType installation');
}
$labelBox = imagettfbbox($label->getFont()->getSize(), 0, $label->getFont()->getPath(), $label->getText());
if (!is_array($labelBox)) {
throw new \Exception('Unable to generate label image box: check your FreeType installation');
}
return new self(
intval($labelBox[2] - $labelBox[0]),
intval($labelBox[0] - $labelBox[7])
);
}
public function getWidth(): int
{
return $this->width;
}
public function getHeight(): int
{
return $this->height;
}
}

View File

@ -0,0 +1,164 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\ImageData;
use Endroid\QrCode\Logo\LogoInterface;
class LogoImageData
{
private string $data;
/** @var mixed */
private $image;
private string $mimeType;
private int $width;
private int $height;
private bool $punchoutBackground;
/** @param mixed $image */
private function __construct(
string $data,
$image,
string $mimeType,
int $width,
int $height,
bool $punchoutBackground
) {
$this->data = $data;
$this->image = $image;
$this->mimeType = $mimeType;
$this->width = $width;
$this->height = $height;
$this->punchoutBackground = $punchoutBackground;
}
public static function createForLogo(LogoInterface $logo): self
{
$data = @file_get_contents($logo->getPath());
if (!is_string($data)) {
throw new \Exception(sprintf('Invalid data at path "%s"', $logo->getPath()));
}
if (false !== filter_var($logo->getPath(), FILTER_VALIDATE_URL)) {
$mimeType = self::detectMimeTypeFromUrl($logo->getPath());
} else {
$mimeType = self::detectMimeTypeFromPath($logo->getPath());
}
$width = $logo->getResizeToWidth();
$height = $logo->getResizeToHeight();
if ('image/svg+xml' === $mimeType) {
if (null === $width || null === $height) {
throw new \Exception('SVG Logos require an explicitly set resize width and height');
}
return new self($data, null, $mimeType, $width, $height, $logo->getPunchoutBackground());
}
$image = @imagecreatefromstring($data);
if (!$image) {
throw new \Exception(sprintf('Unable to parse image data at path "%s"', $logo->getPath()));
}
// No target width and height specified: use from original image
if (null !== $width && null !== $height) {
return new self($data, $image, $mimeType, $width, $height, $logo->getPunchoutBackground());
}
// Only target width specified: calculate height
if (null !== $width && null === $height) {
return new self($data, $image, $mimeType, $width, intval(imagesy($image) * $width / imagesx($image)), $logo->getPunchoutBackground());
}
// Only target height specified: calculate width
if (null === $width && null !== $height) {
return new self($data, $image, $mimeType, intval(imagesx($image) * $height / imagesy($image)), $height, $logo->getPunchoutBackground());
}
return new self($data, $image, $mimeType, imagesx($image), imagesy($image), $logo->getPunchoutBackground());
}
public function getData(): string
{
return $this->data;
}
/** @return mixed */
public function getImage()
{
if (null === $this->image) {
throw new \Exception('SVG Images have no image resource');
}
return $this->image;
}
public function getMimeType(): string
{
return $this->mimeType;
}
public function getWidth(): int
{
return $this->width;
}
public function getHeight(): int
{
return $this->height;
}
public function getPunchoutBackground(): bool
{
return $this->punchoutBackground;
}
public function createDataUri(): string
{
return 'data:'.$this->mimeType.';base64,'.base64_encode($this->data);
}
private static function detectMimeTypeFromUrl(string $url): string
{
/** @var mixed $format */
$format = PHP_VERSION_ID >= 80000 ? true : 1;
$headers = get_headers($url, $format);
if (!is_array($headers) || !isset($headers['Content-Type'])) {
throw new \Exception(sprintf('Content type could not be determined for logo URL "%s"', $url));
}
return is_array($headers['Content-Type']) ? $headers['Content-Type'][1] : $headers['Content-Type'];
}
private static function detectMimeTypeFromPath(string $path): string
{
if (!function_exists('mime_content_type')) {
throw new \Exception('You need the ext-fileinfo extension to determine logo mime type');
}
$mimeType = @mime_content_type($path);
if (!is_string($mimeType)) {
throw new \Exception('Could not determine mime type');
}
if (!preg_match('#^image/#', $mimeType)) {
throw new \Exception('Logo path is not an image');
}
// Passing mime type image/svg results in invisible images
if ('image/svg' === $mimeType) {
return 'image/svg+xml';
}
return $mimeType;
}
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Alignment;
final class LabelAlignmentCenter implements LabelAlignmentInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Alignment;
interface LabelAlignmentInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Alignment;
final class LabelAlignmentLeft implements LabelAlignmentInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Alignment;
final class LabelAlignmentRight implements LabelAlignmentInterface
{
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Font;
final class Font implements FontInterface
{
private string $path;
private int $size;
public function __construct(string $path, int $size = 16)
{
$this->validatePath($path);
$this->path = $path;
$this->size = $size;
}
private function validatePath(string $path): void
{
if (!file_exists($path)) {
throw new \Exception(sprintf('Invalid font path "%s"', $path));
}
}
public function getPath(): string
{
return $this->path;
}
public function getSize(): int
{
return $this->size;
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Font;
interface FontInterface
{
public function getPath(): string;
public function getSize(): int;
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Font;
final class NotoSans implements FontInterface
{
private int $size;
public function __construct(int $size = 16)
{
$this->size = $size;
}
public function getPath(): string
{
return __DIR__.'/../../../assets/noto_sans.otf';
}
public function getSize(): int
{
return $this->size;
}
}

View File

@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Font;
final class OpenSans implements FontInterface
{
private int $size;
public function __construct(int $size = 16)
{
$this->size = $size;
}
public function getPath(): string
{
return __DIR__.'/../../../assets/open_sans.ttf';
}
public function getSize(): int
{
return $this->size;
}
}

View File

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label;
use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Color\ColorInterface;
use Endroid\QrCode\Label\Alignment\LabelAlignmentCenter;
use Endroid\QrCode\Label\Alignment\LabelAlignmentInterface;
use Endroid\QrCode\Label\Font\Font;
use Endroid\QrCode\Label\Font\FontInterface;
use Endroid\QrCode\Label\Margin\Margin;
use Endroid\QrCode\Label\Margin\MarginInterface;
final class Label implements LabelInterface
{
private string $text;
private FontInterface $font;
private LabelAlignmentInterface $alignment;
private MarginInterface $margin;
private ColorInterface $textColor;
public function __construct(
string $text,
FontInterface $font = null,
LabelAlignmentInterface $alignment = null,
MarginInterface $margin = null,
ColorInterface $textColor = null
) {
$this->text = $text;
$this->font = isset($font) ? $font : new Font(__DIR__.'/../../assets/noto_sans.otf', 16);
$this->alignment = isset($alignment) ? $alignment : new LabelAlignmentCenter();
$this->margin = isset($margin) ? $margin : new Margin(0, 10, 10, 10);
$this->textColor = isset($textColor) ? $textColor : new Color(0, 0, 0);
}
public static function create(string $text): self
{
return new self($text);
}
public function getText(): string
{
return $this->text;
}
public function setText(string $text): self
{
$this->text = $text;
return $this;
}
public function getFont(): FontInterface
{
return $this->font;
}
public function setFont(FontInterface $font): self
{
$this->font = $font;
return $this;
}
public function getAlignment(): LabelAlignmentInterface
{
return $this->alignment;
}
public function setAlignment(LabelAlignmentInterface $alignment): self
{
$this->alignment = $alignment;
return $this;
}
public function getMargin(): MarginInterface
{
return $this->margin;
}
public function setMargin(MarginInterface $margin): self
{
$this->margin = $margin;
return $this;
}
public function getTextColor(): ColorInterface
{
return $this->textColor;
}
public function setTextColor(ColorInterface $textColor): self
{
$this->textColor = $textColor;
return $this;
}
}

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label;
use Endroid\QrCode\Color\ColorInterface;
use Endroid\QrCode\Label\Alignment\LabelAlignmentInterface;
use Endroid\QrCode\Label\Font\FontInterface;
use Endroid\QrCode\Label\Margin\MarginInterface;
interface LabelInterface
{
public function getText(): string;
public function getFont(): FontInterface;
public function getAlignment(): LabelAlignmentInterface;
public function getMargin(): MarginInterface;
public function getTextColor(): ColorInterface;
}

View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Margin;
final class Margin implements MarginInterface
{
private int $top;
private int $right;
private int $bottom;
private int $left;
public function __construct(int $top, int $right, int $bottom, int $left)
{
$this->top = $top;
$this->right = $right;
$this->bottom = $bottom;
$this->left = $left;
}
public function getTop(): int
{
return $this->top;
}
public function getRight(): int
{
return $this->right;
}
public function getBottom(): int
{
return $this->bottom;
}
public function getLeft(): int
{
return $this->left;
}
/** @return array<string, int> */
public function toArray(): array
{
return [
'top' => $this->top,
'right' => $this->right,
'bottom' => $this->bottom,
'left' => $this->left,
];
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Label\Margin;
interface MarginInterface
{
public function getTop(): int;
public function getRight(): int;
public function getBottom(): int;
public function getLeft(): int;
/** @return array<string, int> */
public function toArray(): array;
}

View File

@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Logo;
final class Logo implements LogoInterface
{
private string $path;
private ?int $resizeToWidth;
private ?int $resizeToHeight;
private bool $punchoutBackground;
public function __construct(string $path, ?int $resizeToWidth = null, ?int $resizeToHeight = null, bool $punchoutBackground = false)
{
$this->path = $path;
$this->resizeToWidth = $resizeToWidth;
$this->resizeToHeight = $resizeToHeight;
$this->punchoutBackground = $punchoutBackground;
}
public static function create(string $path): self
{
return new self($path);
}
public function getPath(): string
{
return $this->path;
}
public function setPath(string $path): self
{
$this->path = $path;
return $this;
}
public function getResizeToWidth(): ?int
{
return $this->resizeToWidth;
}
public function setResizeToWidth(?int $resizeToWidth): self
{
$this->resizeToWidth = $resizeToWidth;
return $this;
}
public function getResizeToHeight(): ?int
{
return $this->resizeToHeight;
}
public function setResizeToHeight(?int $resizeToHeight): self
{
$this->resizeToHeight = $resizeToHeight;
return $this;
}
public function getPunchoutBackground(): bool
{
return $this->punchoutBackground;
}
public function setPunchoutBackground(bool $punchoutBackground): self
{
$this->punchoutBackground = $punchoutBackground;
return $this;
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Logo;
interface LogoInterface
{
public function getPath(): string;
public function getResizeToWidth(): ?int;
public function getResizeToHeight(): ?int;
public function getPunchoutBackground(): bool;
}

View File

@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Matrix;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeEnlarge;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeInterface;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeShrink;
final class Matrix implements MatrixInterface
{
/** @var array<int, array<int, int>> */
private array $blockValues = [];
private float $blockSize;
private int $innerSize;
private int $outerSize;
private int $marginLeft;
private int $marginRight;
/** @param array<array<int>> $blockValues */
public function __construct(array $blockValues, int $size, int $margin, RoundBlockSizeModeInterface $roundBlockSizeMode)
{
$this->blockValues = $blockValues;
$this->blockSize = $size / $this->getBlockCount();
$this->innerSize = $size;
$this->outerSize = $size + 2 * $margin;
if ($roundBlockSizeMode instanceof RoundBlockSizeModeEnlarge) {
$this->blockSize = intval(ceil($this->blockSize));
$this->innerSize = intval($this->blockSize * $this->getBlockCount());
$this->outerSize = $this->innerSize + 2 * $margin;
} elseif ($roundBlockSizeMode instanceof RoundBlockSizeModeShrink) {
$this->blockSize = intval(floor($this->blockSize));
$this->innerSize = intval($this->blockSize * $this->getBlockCount());
$this->outerSize = $this->innerSize + 2 * $margin;
} elseif ($roundBlockSizeMode instanceof RoundBlockSizeModeMargin) {
$this->blockSize = intval(floor($this->blockSize));
$this->innerSize = intval($this->blockSize * $this->getBlockCount());
}
if ($this->blockSize < 1) {
throw new \Exception('Too much data: increase image dimensions or lower error correction level');
}
$this->marginLeft = intval(($this->outerSize - $this->innerSize) / 2);
$this->marginRight = $this->outerSize - $this->innerSize - $this->marginLeft;
}
public function getBlockValue(int $rowIndex, int $columnIndex): int
{
return $this->blockValues[$rowIndex][$columnIndex];
}
public function getBlockCount(): int
{
return count($this->blockValues[0]);
}
public function getBlockSize(): float
{
return $this->blockSize;
}
public function getInnerSize(): int
{
return $this->innerSize;
}
public function getOuterSize(): int
{
return $this->outerSize;
}
public function getMarginLeft(): int
{
return $this->marginLeft;
}
public function getMarginRight(): int
{
return $this->marginRight;
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Matrix;
use Endroid\QrCode\QrCodeInterface;
interface MatrixFactoryInterface
{
public function create(QrCodeInterface $qrCode): MatrixInterface;
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Matrix;
interface MatrixInterface
{
public function getBlockValue(int $rowIndex, int $columnIndex): int;
public function getBlockCount(): int;
public function getBlockSize(): float;
public function getInnerSize(): int;
public function getOuterSize(): int;
public function getMarginLeft(): int;
public function getMarginRight(): int;
}

View File

@ -2,119 +2,87 @@
declare(strict_types=1); declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode; namespace Endroid\QrCode;
use BaconQrCode\Encoder\Encoder; use Endroid\QrCode\Color\Color;
use Endroid\QrCode\Exception\InvalidFontException; use Endroid\QrCode\Color\ColorInterface;
use Endroid\QrCode\Exception\UnsupportedExtensionException; use Endroid\QrCode\Encoding\Encoding;
use Endroid\QrCode\Writer\WriterInterface; use Endroid\QrCode\Encoding\EncodingInterface;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelLow;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeInterface;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeMargin;
class QrCode implements QrCodeInterface final class QrCode implements QrCodeInterface
{ {
const LABEL_FONT_PATH_DEFAULT = __DIR__.'/../assets/fonts/noto_sans.otf'; private string $data;
private EncodingInterface $encoding;
private ErrorCorrectionLevelInterface $errorCorrectionLevel;
private int $size;
private int $margin;
private RoundBlockSizeModeInterface $roundBlockSizeMode;
private ColorInterface $foregroundColor;
private ColorInterface $backgroundColor;
private $text; public function __construct(
string $data,
/** @var int */ EncodingInterface $encoding = null,
private $size = 300; ErrorCorrectionLevelInterface $errorCorrectionLevel = null,
int $size = 300,
/** @var int */ int $margin = 10,
private $margin = 10; RoundBlockSizeModeInterface $roundBlockSizeMode = null,
ColorInterface $foregroundColor = null,
/** @var array */ ColorInterface $backgroundColor = null
private $foregroundColor = [ ) {
'r' => 0, $this->data = $data;
'g' => 0, $this->encoding = $encoding ?? new Encoding('UTF-8');
'b' => 0, $this->errorCorrectionLevel = $errorCorrectionLevel ?? new ErrorCorrectionLevelLow();
'a' => 0,
];
/** @var array */
private $backgroundColor = [
'r' => 255,
'g' => 255,
'b' => 255,
'a' => 0,
];
/** @var string */
private $encoding = 'UTF-8';
/** @var bool */
private $roundBlockSize = true;
private $errorCorrectionLevel;
/** @var string */
private $logoPath;
/** @var int|null */
private $logoWidth;
/** @var int|null */
private $logoHeight;
/** @var string */
private $label;
/** @var int */
private $labelFontSize = 16;
/** @var string */
private $labelFontPath = self::LABEL_FONT_PATH_DEFAULT;
private $labelAlignment;
/** @var array */
private $labelMargin = [
't' => 0,
'r' => 10,
'b' => 10,
'l' => 10,
];
/** @var WriterRegistryInterface */
private $writerRegistry;
/** @var WriterInterface|null */
private $writer;
/** @var array */
private $writerOptions = [];
/** @var bool */
private $validateResult = false;
public function __construct(string $text = '')
{
$this->text = $text;
$this->errorCorrectionLevel = ErrorCorrectionLevel::LOW();
$this->labelAlignment = LabelAlignment::CENTER();
$this->createWriterRegistry();
}
public function setText(string $text): void
{
$this->text = $text;
}
public function getText(): string
{
return $this->text;
}
public function setSize(int $size): void
{
$this->size = $size; $this->size = $size;
$this->margin = $margin;
$this->roundBlockSizeMode = $roundBlockSizeMode ?? new RoundBlockSizeModeMargin();
$this->foregroundColor = $foregroundColor ?? new Color(0, 0, 0);
$this->backgroundColor = $backgroundColor ?? new Color(255, 255, 255);
}
public static function create(string $data): self
{
return new self($data);
}
public function getData(): string
{
return $this->data;
}
public function setData(string $data): self
{
$this->data = $data;
return $this;
}
public function getEncoding(): EncodingInterface
{
return $this->encoding;
}
public function setEncoding(Encoding $encoding): self
{
$this->encoding = $encoding;
return $this;
}
public function getErrorCorrectionLevel(): ErrorCorrectionLevelInterface
{
return $this->errorCorrectionLevel;
}
public function setErrorCorrectionLevel(ErrorCorrectionLevelInterface $errorCorrectionLevel): self
{
$this->errorCorrectionLevel = $errorCorrectionLevel;
return $this;
} }
public function getSize(): int public function getSize(): int
@ -122,9 +90,11 @@ class QrCode implements QrCodeInterface
return $this->size; return $this->size;
} }
public function setMargin(int $margin): void public function setSize(int $size): self
{ {
$this->margin = $margin; $this->size = $size;
return $this;
} }
public function getMargin(): int public function getMargin(): int
@ -132,308 +102,46 @@ class QrCode implements QrCodeInterface
return $this->margin; return $this->margin;
} }
public function setForegroundColor(array $foregroundColor): void public function setMargin(int $margin): self
{ {
if (!isset($foregroundColor['a'])) { $this->margin = $margin;
$foregroundColor['a'] = 0;
return $this;
} }
foreach ($foregroundColor as &$color) { public function getRoundBlockSizeMode(): RoundBlockSizeModeInterface
$color = intval($color); {
return $this->roundBlockSizeMode;
} }
$this->foregroundColor = $foregroundColor; public function setRoundBlockSizeMode(RoundBlockSizeModeInterface $roundBlockSizeMode): self
{
$this->roundBlockSizeMode = $roundBlockSizeMode;
return $this;
} }
public function getForegroundColor(): array public function getForegroundColor(): ColorInterface
{ {
return $this->foregroundColor; return $this->foregroundColor;
} }
public function setBackgroundColor(array $backgroundColor): void public function setForegroundColor(ColorInterface $foregroundColor): self
{ {
if (!isset($backgroundColor['a'])) { $this->foregroundColor = $foregroundColor;
$backgroundColor['a'] = 0;
return $this;
} }
foreach ($backgroundColor as &$color) { public function getBackgroundColor(): ColorInterface
$color = intval($color);
}
$this->backgroundColor = $backgroundColor;
}
public function getBackgroundColor(): array
{ {
return $this->backgroundColor; return $this->backgroundColor;
} }
public function setEncoding(string $encoding): void public function setBackgroundColor(ColorInterface $backgroundColor): self
{ {
$this->encoding = $encoding; $this->backgroundColor = $backgroundColor;
}
public function getEncoding(): string return $this;
{
return $this->encoding;
}
public function setRoundBlockSize(bool $roundBlockSize): void
{
$this->roundBlockSize = $roundBlockSize;
}
public function getRoundBlockSize(): bool
{
return $this->roundBlockSize;
}
public function setErrorCorrectionLevel(ErrorCorrectionLevel $errorCorrectionLevel): void
{
$this->errorCorrectionLevel = $errorCorrectionLevel;
}
public function getErrorCorrectionLevel(): ErrorCorrectionLevel
{
return $this->errorCorrectionLevel;
}
public function setLogoPath(string $logoPath): void
{
$this->logoPath = $logoPath;
}
public function getLogoPath(): ?string
{
return $this->logoPath;
}
public function setLogoSize(int $logoWidth, int $logoHeight = null): void
{
$this->logoWidth = $logoWidth;
$this->logoHeight = $logoHeight;
}
public function setLogoWidth(int $logoWidth): void
{
$this->logoWidth = $logoWidth;
}
public function getLogoWidth(): ?int
{
return $this->logoWidth;
}
public function setLogoHeight(int $logoHeight): void
{
$this->logoHeight = $logoHeight;
}
public function getLogoHeight(): ?int
{
return $this->logoHeight;
}
public function setLabel(string $label, int $labelFontSize = null, string $labelFontPath = null, string $labelAlignment = null, array $labelMargin = null): void
{
$this->label = $label;
if (null !== $labelFontSize) {
$this->setLabelFontSize($labelFontSize);
}
if (null !== $labelFontPath) {
$this->setLabelFontPath($labelFontPath);
}
if (null !== $labelAlignment) {
$this->setLabelAlignment($labelAlignment);
}
if (null !== $labelMargin) {
$this->setLabelMargin($labelMargin);
}
}
public function getLabel(): ?string
{
return $this->label;
}
public function setLabelFontSize(int $labelFontSize): void
{
$this->labelFontSize = $labelFontSize;
}
public function getLabelFontSize(): int
{
return $this->labelFontSize;
}
public function setLabelFontPath(string $labelFontPath): void
{
$resolvedLabelFontPath = (string) realpath($labelFontPath);
if (!is_file($resolvedLabelFontPath)) {
throw new InvalidFontException('Invalid label font path: '.$labelFontPath);
}
$this->labelFontPath = $resolvedLabelFontPath;
}
public function getLabelFontPath(): string
{
return $this->labelFontPath;
}
public function setLabelAlignment(string $labelAlignment): void
{
$this->labelAlignment = new LabelAlignment($labelAlignment);
}
public function getLabelAlignment(): string
{
return $this->labelAlignment->getValue();
}
public function setLabelMargin(array $labelMargin): void
{
$this->labelMargin = array_merge($this->labelMargin, $labelMargin);
}
public function getLabelMargin(): array
{
return $this->labelMargin;
}
public function setWriterRegistry(WriterRegistryInterface $writerRegistry): void
{
$this->writerRegistry = $writerRegistry;
}
public function setWriter(WriterInterface $writer): void
{
$this->writer = $writer;
}
public function getWriter(string $name = null): WriterInterface
{
if (!is_null($name)) {
return $this->writerRegistry->getWriter($name);
}
if ($this->writer instanceof WriterInterface) {
return $this->writer;
}
return $this->writerRegistry->getDefaultWriter();
}
public function setWriterOptions(array $writerOptions): void
{
$this->writerOptions = $writerOptions;
}
public function getWriterOptions(): array
{
return $this->writerOptions;
}
private function createWriterRegistry(): void
{
$this->writerRegistry = new WriterRegistry();
$this->writerRegistry->loadDefaultWriters();
}
public function setWriterByName(string $name): void
{
$this->writer = $this->getWriter($name);
}
public function setWriterByPath(string $path): void
{
$extension = pathinfo($path, PATHINFO_EXTENSION);
$this->setWriterByExtension($extension);
}
public function setWriterByExtension(string $extension): void
{
foreach ($this->writerRegistry->getWriters() as $writer) {
if ($writer->supportsExtension($extension)) {
$this->writer = $writer;
return;
}
}
throw new UnsupportedExtensionException('Missing writer for extension "'.$extension.'"');
}
public function writeString(): string
{
return $this->getWriter()->writeString($this);
}
public function writeDataUri(): string
{
return $this->getWriter()->writeDataUri($this);
}
public function writeFile(string $path): void
{
$this->getWriter()->writeFile($this, $path);
}
public function getContentType(): string
{
return $this->getWriter()->getContentType();
}
public function setValidateResult(bool $validateResult): void
{
$this->validateResult = $validateResult;
}
public function getValidateResult(): bool
{
return $this->validateResult;
}
public function getData(): array
{
$baconErrorCorrectionLevel = $this->errorCorrectionLevel->toBaconErrorCorrectionLevel();
$baconQrCode = Encoder::encode($this->text, $baconErrorCorrectionLevel, $this->encoding);
$baconMatrix = $baconQrCode->getMatrix();
$matrix = [];
$columnCount = $baconMatrix->getWidth();
$rowCount = $baconMatrix->getHeight();
for ($rowIndex = 0; $rowIndex < $rowCount; ++$rowIndex) {
$matrix[$rowIndex] = [];
for ($columnIndex = 0; $columnIndex < $columnCount; ++$columnIndex) {
$matrix[$rowIndex][$columnIndex] = $baconMatrix->get($columnIndex, $rowIndex);
}
}
$data = ['matrix' => $matrix];
$data['block_count'] = count($matrix[0]);
$data['block_size'] = $this->size / $data['block_count'];
if ($this->roundBlockSize) {
$data['block_size'] = intval(floor($data['block_size']));
}
$data['inner_width'] = $data['block_size'] * $data['block_count'];
$data['inner_height'] = $data['block_size'] * $data['block_count'];
$data['outer_width'] = $this->size + 2 * $this->margin;
$data['outer_height'] = $this->size + 2 * $this->margin;
$data['margin_left'] = ($data['outer_width'] - $data['inner_width']) / 2;
if ($this->roundBlockSize) {
$data['margin_left'] = intval(floor($data['margin_left']));
}
$data['margin_right'] = $data['outer_width'] - $data['inner_width'] - $data['margin_left'];
return $data;
} }
} }

View File

@ -2,62 +2,28 @@
declare(strict_types=1); declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode; namespace Endroid\QrCode;
use Endroid\QrCode\Color\ColorInterface;
use Endroid\QrCode\Encoding\EncodingInterface;
use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelInterface;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeInterface;
interface QrCodeInterface interface QrCodeInterface
{ {
public function getText(): string; public function getData(): string;
public function getEncoding(): EncodingInterface;
public function getErrorCorrectionLevel(): ErrorCorrectionLevelInterface;
public function getSize(): int; public function getSize(): int;
public function getMargin(): int; public function getMargin(): int;
public function getForegroundColor(): array; public function getRoundBlockSizeMode(): RoundBlockSizeModeInterface;
public function getBackgroundColor(): array; public function getForegroundColor(): ColorInterface;
public function getEncoding(): string; public function getBackgroundColor(): ColorInterface;
public function getRoundBlockSize(): bool;
public function getErrorCorrectionLevel(): ErrorCorrectionLevel;
public function getLogoPath(): ?string;
public function getLogoWidth(): ?int;
public function getLogoHeight(): ?int;
public function getLabel(): ?string;
public function getLabelFontPath(): string;
public function getLabelFontSize(): int;
public function getLabelAlignment(): string;
public function getLabelMargin(): array;
public function getValidateResult(): bool;
public function getWriterOptions(): array;
public function getContentType(): string;
public function setWriterRegistry(WriterRegistryInterface $writerRegistry): void;
public function writeString(): string;
public function writeDataUri(): string;
public function writeFile(string $path): void;
public function getData(): array;
} }

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\RoundBlockSizeMode;
final class RoundBlockSizeModeEnlarge implements RoundBlockSizeModeInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\RoundBlockSizeMode;
interface RoundBlockSizeModeInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\RoundBlockSizeMode;
final class RoundBlockSizeModeMargin implements RoundBlockSizeModeInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\RoundBlockSizeMode;
final class RoundBlockSizeModeNone implements RoundBlockSizeModeInterface
{
}

View File

@ -0,0 +1,9 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\RoundBlockSizeMode;
final class RoundBlockSizeModeShrink implements RoundBlockSizeModeInterface
{
}

View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
interface WritableInterface
{
}

View File

@ -2,46 +2,22 @@
declare(strict_types=1); declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer; namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Bacon\MatrixFactory;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface; use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\Writer\Result\BinaryResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
class BinaryWriter extends AbstractWriter final class BinaryWriter implements WriterInterface
{ {
public function writeString(QrCodeInterface $qrCode): string public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
{ {
$rows = []; $matrixFactory = new MatrixFactory();
$data = $qrCode->getData(); $matrix = $matrixFactory->create($qrCode);
foreach ($data['matrix'] as $row) {
$values = '';
foreach ($row as $value) {
$values .= $value;
}
$rows[] = $values;
}
return implode("\n", $rows); return new BinaryResult($matrix);
}
public static function getContentType(): string
{
return 'text/plain';
}
public static function getSupportedExtensions(): array
{
return ['bin', 'txt'];
}
public function getName(): string
{
return 'binary';
} }
} }

View File

@ -2,59 +2,27 @@
declare(strict_types=1); declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer; namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface; use Endroid\QrCode\QrCodeInterface;
use Exception; use Endroid\QrCode\Writer\Result\DebugResult;
use ReflectionClass; use Endroid\QrCode\Writer\Result\ResultInterface;
class DebugWriter extends AbstractWriter final class DebugWriter implements WriterInterface, ValidatingWriterInterface
{ {
public function writeString(QrCodeInterface $qrCode): string public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
{ {
$data = []; return new DebugResult($qrCode, $logo, $label, $options);
$skip = ['getData'];
$reflectionClass = new ReflectionClass($qrCode);
foreach ($reflectionClass->getMethods() as $method) {
$methodName = $method->getShortName();
if (0 === strpos($methodName, 'get') && 0 == $method->getNumberOfParameters() && !in_array($methodName, $skip)) {
$value = $qrCode->{$methodName}();
if (is_array($value) && !is_object(current($value))) {
$value = '['.implode(', ', $value).']';
} elseif (is_bool($value)) {
$value = $value ? 'true' : 'false';
} elseif (is_string($value)) {
$value = '"'.$value.'"';
} elseif (is_null($value)) {
$value = 'null';
}
try {
$data[] = $methodName.': '.$value;
} catch (Exception $exception) {
}
}
} }
$string = implode(" \n", $data); public function validateResult(ResultInterface $result, string $expectedData): void
return $string;
}
public static function getContentType(): string
{ {
return 'text/plain'; if (!$result instanceof DebugResult) {
throw new \Exception('Unable to write logo: instance of DebugResult expected');
} }
public function getName(): string $result->setValidateResult(true);
{
return 'debug';
} }
} }

View File

@ -2,58 +2,43 @@
declare(strict_types=1); declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer; namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Bacon\MatrixFactory;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface; use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\Writer\Result\EpsResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
class EpsWriter extends AbstractWriter final class EpsWriter implements WriterInterface
{ {
public function writeString(QrCodeInterface $qrCode): string public const DECIMAL_PRECISION = 10;
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
{ {
$data = $qrCode->getData(); $matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);
$epsData = []; $lines = [
$epsData[] = '%!PS-Adobe-3.0 EPSF-3.0'; '%!PS-Adobe-3.0 EPSF-3.0',
$epsData[] = '%%BoundingBox: 0 0 '.$data['outer_width'].' '.$data['outer_height']; '%%BoundingBox: 0 0 '.$matrix->getOuterSize().' '.$matrix->getOuterSize(),
$epsData[] = '/F { rectfill } def'; '/F { rectfill } def',
$epsData[] = number_format($qrCode->getBackgroundColor()['r'] / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()['g'] / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()['b'] / 100, 2, '.', ',').' setrgbcolor'; number_format($qrCode->getBackgroundColor()->getRed() / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()->getGreen() / 100, 2, '.', ',').' '.number_format($qrCode->getBackgroundColor()->getBlue() / 100, 2, '.', ',').' setrgbcolor',
$epsData[] = '0 0 '.$data['outer_width'].' '.$data['outer_height'].' F'; '0 0 '.$matrix->getOuterSize().' '.$matrix->getOuterSize().' F',
$epsData[] = number_format($qrCode->getForegroundColor()['r'] / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()['g'] / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()['b'] / 100, 2, '.', ',').' setrgbcolor'; number_format($qrCode->getForegroundColor()->getRed() / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()->getGreen() / 100, 2, '.', ',').' '.number_format($qrCode->getForegroundColor()->getBlue() / 100, 2, '.', ',').' setrgbcolor',
];
// Please note an EPS has a reversed Y axis compared to PNG and SVG for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) {
$data['matrix'] = array_reverse($data['matrix']); for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) {
foreach ($data['matrix'] as $row => $values) { if (1 === $matrix->getBlockValue($matrix->getBlockCount() - 1 - $rowIndex, $columnIndex)) {
foreach ($values as $column => $value) { $x = $matrix->getMarginLeft() + $matrix->getBlockSize() * $columnIndex;
if (1 === $value) { $y = $matrix->getMarginLeft() + $matrix->getBlockSize() * $rowIndex;
$x = $data['margin_left'] + $data['block_size'] * $column; $lines[] = number_format($x, self::DECIMAL_PRECISION, '.', '').' '.number_format($y, self::DECIMAL_PRECISION, '.', '').' '.number_format($matrix->getBlockSize(), self::DECIMAL_PRECISION, '.', '').' '.number_format($matrix->getBlockSize(), self::DECIMAL_PRECISION, '.', '').' F';
$y = $data['margin_left'] + $data['block_size'] * $row;
$epsData[] = $x.' '.$y.' '.$data['block_size'].' '.$data['block_size'].' F';
} }
} }
} }
return implode("\n", $epsData); return new EpsResult($lines);
}
public static function getContentType(): string
{
return 'image/eps';
}
public static function getSupportedExtensions(): array
{
return ['eps'];
}
public function getName(): string
{
return 'eps';
} }
} }

View File

@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Bacon\MatrixFactory;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\Writer\Result\PdfResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
final class PdfWriter implements WriterInterface
{
public const WRITER_OPTION_UNIT = 'unit';
public const WRITER_OPTION_PDF = 'fpdf';
public const WRITER_OPTION_X = 'x';
public const WRITER_OPTION_Y = 'y';
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
{
$matrixFactory = new MatrixFactory();
$matrix = $matrixFactory->create($qrCode);
$unit = 'mm';
if (isset($options[self::WRITER_OPTION_UNIT])) {
$unit = $options[self::WRITER_OPTION_UNIT];
}
$allowedUnits = ['mm', 'pt', 'cm', 'in'];
if (!in_array($unit, $allowedUnits)) {
throw new \Exception(sprintf('PDF Measure unit should be one of [%s]', implode(', ', $allowedUnits)));
}
$labelSpace = 0;
if ($label instanceof LabelInterface) {
$labelSpace = 30;
}
if (!class_exists(\FPDF::class)) {
throw new \Exception('Unable to find FPDF: check your installation');
}
$foregroundColor = $qrCode->getForegroundColor();
if ($foregroundColor->getAlpha() > 0) {
throw new \Exception('PDF Writer does not support alpha channels');
}
$backgroundColor = $qrCode->getBackgroundColor();
if ($backgroundColor->getAlpha() > 0) {
throw new \Exception('PDF Writer does not support alpha channels');
}
if (isset($options[self::WRITER_OPTION_PDF])) {
$fpdf = $options[self::WRITER_OPTION_PDF];
if (!$fpdf instanceof \FPDF) {
throw new \Exception('pdf option must be an instance of FPDF');
}
} else {
// @todo Check how to add label height later
$fpdf = new \FPDF('P', $unit, [$matrix->getOuterSize(), $matrix->getOuterSize() + $labelSpace]);
$fpdf->AddPage();
}
$x = 0;
if (isset($options[self::WRITER_OPTION_X])) {
$x = $options[self::WRITER_OPTION_X];
}
$y = 0;
if (isset($options[self::WRITER_OPTION_Y])) {
$y = $options[self::WRITER_OPTION_Y];
}
$fpdf->SetFillColor($backgroundColor->getRed(), $backgroundColor->getGreen(), $backgroundColor->getBlue());
$fpdf->Rect($x, $y, $matrix->getOuterSize(), $matrix->getOuterSize(), 'F');
$fpdf->SetFillColor($foregroundColor->getRed(), $foregroundColor->getGreen(), $foregroundColor->getBlue());
for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) {
for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) {
if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) {
$fpdf->Rect(
$x + $matrix->getMarginLeft() + ($columnIndex * $matrix->getBlockSize()),
$y + $matrix->getMarginLeft() + ($rowIndex * $matrix->getBlockSize()),
$matrix->getBlockSize(),
$matrix->getBlockSize(),
'F'
);
}
}
}
if ($logo instanceof LogoInterface) {
$this->addLogo($logo, $fpdf, $x, $y, $matrix->getOuterSize());
}
if ($label instanceof LabelInterface) {
$fpdf->SetXY($x, $y + $matrix->getOuterSize() + $labelSpace - 25);
$fpdf->SetFont('Helvetica', '', $label->getFont()->getSize());
$fpdf->Cell($matrix->getOuterSize(), 0, $label->getText(), 0, 0, 'C');
}
return new PdfResult($fpdf);
}
private function addLogo(LogoInterface $logo, \FPDF $fpdf, float $x, float $y, float $size): void
{
$logoPath = $logo->getPath();
$logoHeight = $logo->getResizeToHeight();
$logoWidth = $logo->getResizeToWidth();
if (null === $logoHeight || null === $logoWidth) {
$imageSize = \getimagesize($logoPath);
if (!$imageSize) {
throw new \Exception(sprintf('Unable to read image size for logo "%s"', $logoPath));
}
[$logoSourceWidth, $logoSourceHeight] = $imageSize;
if (null === $logoWidth) {
$logoWidth = (int) $logoSourceWidth;
}
if (null === $logoHeight) {
$aspectRatio = $logoWidth / $logoSourceWidth;
$logoHeight = (int) ($logoSourceHeight * $aspectRatio);
}
}
$logoX = $x + $size / 2 - $logoWidth / 2;
$logoY = $y + $size / 2 - $logoHeight / 2;
$fpdf->Image($logoPath, $logoX, $logoY, $logoWidth, $logoHeight);
}
}

View File

@ -2,209 +2,226 @@
declare(strict_types=1); declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer; namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Exception\GenerateImageException; use Endroid\QrCode\Bacon\MatrixFactory;
use Endroid\QrCode\Exception\MissingFunctionException; use Endroid\QrCode\ImageData\LabelImageData;
use Endroid\QrCode\Exception\MissingLogoHeightException; use Endroid\QrCode\ImageData\LogoImageData;
use Endroid\QrCode\Exception\ValidationException; use Endroid\QrCode\Label\Alignment\LabelAlignmentLeft;
use Endroid\QrCode\LabelAlignment; use Endroid\QrCode\Label\Alignment\LabelAlignmentRight;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface; use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeNone;
use Endroid\QrCode\Writer\Result\PngResult;
use Endroid\QrCode\Writer\Result\ResultInterface;
use Zxing\QrReader; use Zxing\QrReader;
class PngWriter extends AbstractWriter final class PngWriter implements WriterInterface, ValidatingWriterInterface
{ {
public function writeString(QrCodeInterface $qrCode): string public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
{ {
$image = $this->createImage($qrCode->getData(), $qrCode); if (!extension_loaded('gd')) {
throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly');
$logoPath = $qrCode->getLogoPath();
if (null !== $logoPath) {
$image = $this->addLogo($image, $logoPath, $qrCode->getLogoWidth(), $qrCode->getLogoHeight());
} }
$label = $qrCode->getLabel(); $matrixFactory = new MatrixFactory();
if (null !== $label) { $matrix = $matrixFactory->create($qrCode);
$image = $this->addLabel($image, $label, $qrCode->getLabelFontPath(), $qrCode->getLabelFontSize(), $qrCode->getLabelAlignment(), $qrCode->getLabelMargin(), $qrCode->getForegroundColor(), $qrCode->getBackgroundColor());
$baseBlockSize = $qrCode->getRoundBlockSizeMode() instanceof RoundBlockSizeModeNone ? 10 : intval($matrix->getBlockSize());
$baseImage = imagecreatetruecolor($matrix->getBlockCount() * $baseBlockSize, $matrix->getBlockCount() * $baseBlockSize);
if (!$baseImage) {
throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly');
} }
$string = $this->imageToString($image); /** @var int $foregroundColor */
$foregroundColor = imagecolorallocatealpha(
$baseImage,
$qrCode->getForegroundColor()->getRed(),
$qrCode->getForegroundColor()->getGreen(),
$qrCode->getForegroundColor()->getBlue(),
$qrCode->getForegroundColor()->getAlpha()
);
imagedestroy($image); /** @var int $transparentColor */
$transparentColor = imagecolorallocatealpha($baseImage, 255, 255, 255, 127);
imagefill($baseImage, 0, 0, $transparentColor);
for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) {
for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) {
if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) {
imagefilledrectangle(
$baseImage,
$columnIndex * $baseBlockSize,
$rowIndex * $baseBlockSize,
($columnIndex + 1) * $baseBlockSize - 1,
($rowIndex + 1) * $baseBlockSize - 1,
$foregroundColor
);
}
}
}
$targetWidth = $matrix->getOuterSize();
$targetHeight = $matrix->getOuterSize();
if ($label instanceof LabelInterface) {
$labelImageData = LabelImageData::createForLabel($label);
$targetHeight += $labelImageData->getHeight() + $label->getMargin()->getTop() + $label->getMargin()->getBottom();
}
$targetImage = imagecreatetruecolor($targetWidth, $targetHeight);
if (!$targetImage) {
throw new \Exception('Unable to generate image: please check if the GD extension is enabled and configured correctly');
}
/** @var int $backgroundColor */
$backgroundColor = imagecolorallocatealpha(
$targetImage,
$qrCode->getBackgroundColor()->getRed(),
$qrCode->getBackgroundColor()->getGreen(),
$qrCode->getBackgroundColor()->getBlue(),
$qrCode->getBackgroundColor()->getAlpha()
);
imagefill($targetImage, 0, 0, $backgroundColor);
imagecopyresampled(
$targetImage,
$baseImage,
$matrix->getMarginLeft(),
$matrix->getMarginLeft(),
0,
0,
$matrix->getInnerSize(),
$matrix->getInnerSize(),
imagesx($baseImage),
imagesy($baseImage)
);
if (PHP_VERSION_ID < 80000) {
imagedestroy($baseImage);
}
if ($qrCode->getBackgroundColor()->getAlpha() > 0) {
imagesavealpha($targetImage, true);
}
$result = new PngResult($targetImage);
if ($logo instanceof LogoInterface) {
$result = $this->addLogo($logo, $result);
}
if ($label instanceof LabelInterface) {
$result = $this->addLabel($label, $result);
}
return $result;
}
private function addLogo(LogoInterface $logo, PngResult $result): PngResult
{
$logoImageData = LogoImageData::createForLogo($logo);
if ('image/svg+xml' === $logoImageData->getMimeType()) {
throw new \Exception('PNG Writer does not support SVG logo');
}
$targetImage = $result->getImage();
if ($logoImageData->getPunchoutBackground()) {
/** @var int $transparent */
$transparent = imagecolorallocatealpha($targetImage, 255, 255, 255, 127);
imagealphablending($targetImage, false);
for (
$x_offset = intval(imagesx($targetImage) / 2 - $logoImageData->getWidth() / 2);
$x_offset < intval(imagesx($targetImage) / 2 - $logoImageData->getWidth() / 2) + $logoImageData->getWidth();
++$x_offset
) {
for (
$y_offset = intval(imagesy($targetImage) / 2 - $logoImageData->getHeight() / 2);
$y_offset < intval(imagesy($targetImage) / 2 - $logoImageData->getHeight() / 2) + $logoImageData->getHeight();
++$y_offset
) {
imagesetpixel(
$targetImage,
$x_offset,
$y_offset,
$transparent
);
}
}
}
imagecopyresampled(
$targetImage,
$logoImageData->getImage(),
intval(imagesx($targetImage) / 2 - $logoImageData->getWidth() / 2),
intval(imagesx($targetImage) / 2 - $logoImageData->getHeight() / 2),
0,
0,
$logoImageData->getWidth(),
$logoImageData->getHeight(),
imagesx($logoImageData->getImage()),
imagesy($logoImageData->getImage())
);
if (PHP_VERSION_ID < 80000) {
imagedestroy($logoImageData->getImage());
}
return new PngResult($targetImage);
}
private function addLabel(LabelInterface $label, PngResult $result): PngResult
{
$targetImage = $result->getImage();
$labelImageData = LabelImageData::createForLabel($label);
/** @var int $textColor */
$textColor = imagecolorallocatealpha(
$targetImage,
$label->getTextColor()->getRed(),
$label->getTextColor()->getGreen(),
$label->getTextColor()->getBlue(),
$label->getTextColor()->getAlpha()
);
$x = intval(imagesx($targetImage) / 2 - $labelImageData->getWidth() / 2);
$y = imagesy($targetImage) - $label->getMargin()->getBottom();
if ($label->getAlignment() instanceof LabelAlignmentLeft) {
$x = $label->getMargin()->getLeft();
} elseif ($label->getAlignment() instanceof LabelAlignmentRight) {
$x = imagesx($targetImage) - $labelImageData->getWidth() - $label->getMargin()->getRight();
}
imagettftext($targetImage, $label->getFont()->getSize(), 0, $x, $y, $textColor, $label->getFont()->getPath(), $label->getText());
return new PngResult($targetImage);
}
public function validateResult(ResultInterface $result, string $expectedData): void
{
$string = $result->getString();
if (!class_exists(QrReader::class)) {
throw new \Exception('Please install khanamiryan/qrcode-detector-decoder or disable image validation');
}
if (PHP_VERSION_ID >= 80000) {
throw new \Exception('The validator is not compatible with PHP 8 yet, see https://github.com/khanamiryan/php-qrcode-detector-decoder/pull/103');
}
if ($qrCode->getValidateResult()) {
$reader = new QrReader($string, QrReader::SOURCE_TYPE_BLOB); $reader = new QrReader($string, QrReader::SOURCE_TYPE_BLOB);
if ($reader->text() !== $qrCode->getText()) { if ($reader->text() !== $expectedData) {
throw new ValidationException('Built-in validation reader read "'.$reader->text().'" instead of "'.$qrCode->getText().'". throw new \Exception('Built-in validation reader read "'.$reader->text().'" instead of "'.$expectedData.'".
Adjust your parameters to increase readability or disable built-in validation.'); Adjust your parameters to increase readability or disable built-in validation.');
} }
} }
return $string;
}
private function createImage(array $data, QrCodeInterface $qrCode)
{
$baseSize = $qrCode->getRoundBlockSize() ? $data['block_size'] : 25;
$baseImage = $this->createBaseImage($baseSize, $data, $qrCode);
$interpolatedImage = $this->createInterpolatedImage($baseImage, $data, $qrCode);
imagedestroy($baseImage);
return $interpolatedImage;
}
private function createBaseImage(int $baseSize, array $data, QrCodeInterface $qrCode)
{
$image = imagecreatetruecolor($data['block_count'] * $baseSize, $data['block_count'] * $baseSize);
if (!is_resource($image)) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$foregroundColor = imagecolorallocatealpha($image, $qrCode->getForegroundColor()['r'], $qrCode->getForegroundColor()['g'], $qrCode->getForegroundColor()['b'], $qrCode->getForegroundColor()['a']);
$backgroundColor = imagecolorallocatealpha($image, $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b'], $qrCode->getBackgroundColor()['a']);
imagefill($image, 0, 0, $backgroundColor);
foreach ($data['matrix'] as $row => $values) {
foreach ($values as $column => $value) {
if (1 === $value) {
imagefilledrectangle($image, $column * $baseSize, $row * $baseSize, intval(($column + 1) * $baseSize), intval(($row + 1) * $baseSize), $foregroundColor);
}
}
}
return $image;
}
private function createInterpolatedImage($baseImage, array $data, QrCodeInterface $qrCode)
{
$image = imagecreatetruecolor($data['outer_width'], $data['outer_height']);
if (!is_resource($image)) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$backgroundColor = imagecolorallocatealpha($image, $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b'], $qrCode->getBackgroundColor()['a']);
imagefill($image, 0, 0, $backgroundColor);
imagecopyresampled($image, $baseImage, (int) $data['margin_left'], (int) $data['margin_left'], 0, 0, (int) $data['inner_width'], (int) $data['inner_height'], imagesx($baseImage), imagesy($baseImage));
imagesavealpha($image, true);
return $image;
}
private function addLogo($sourceImage, string $logoPath, int $logoWidth = null, int $logoHeight = null)
{
$mimeType = $this->getMimeType($logoPath);
$logoImage = imagecreatefromstring(strval(file_get_contents($logoPath)));
if ('image/svg+xml' === $mimeType && (null === $logoHeight || null === $logoWidth)) {
throw new MissingLogoHeightException('SVG Logos require an explicit height set via setLogoSize($width, $height)');
}
if (!is_resource($logoImage)) {
throw new GenerateImageException('Unable to generate image: check your GD installation or logo path');
}
$logoSourceWidth = imagesx($logoImage);
$logoSourceHeight = imagesy($logoImage);
if (null === $logoWidth) {
$logoWidth = $logoSourceWidth;
}
if (null === $logoHeight) {
$aspectRatio = $logoWidth / $logoSourceWidth;
$logoHeight = intval($logoSourceHeight * $aspectRatio);
}
$logoX = imagesx($sourceImage) / 2 - $logoWidth / 2;
$logoY = imagesy($sourceImage) / 2 - $logoHeight / 2;
imagecopyresampled($sourceImage, $logoImage, intval($logoX), intval($logoY), 0, 0, $logoWidth, $logoHeight, $logoSourceWidth, $logoSourceHeight);
imagedestroy($logoImage);
return $sourceImage;
}
private function addLabel($sourceImage, string $label, string $labelFontPath, int $labelFontSize, string $labelAlignment, array $labelMargin, array $foregroundColor, array $backgroundColor)
{
if (!function_exists('imagettfbbox')) {
throw new MissingFunctionException('Missing function "imagettfbbox", please make sure you installed the FreeType library');
}
$labelBox = imagettfbbox($labelFontSize, 0, $labelFontPath, $label);
$labelBoxWidth = intval($labelBox[2] - $labelBox[0]);
$labelBoxHeight = intval($labelBox[0] - $labelBox[7]);
$sourceWidth = imagesx($sourceImage);
$sourceHeight = imagesy($sourceImage);
$targetWidth = $sourceWidth;
$targetHeight = $sourceHeight + $labelBoxHeight + $labelMargin['t'] + $labelMargin['b'];
// Create empty target image
$targetImage = imagecreatetruecolor($targetWidth, $targetHeight);
if (!is_resource($targetImage)) {
throw new GenerateImageException('Unable to generate image: check your GD installation');
}
$foregroundColor = imagecolorallocate($targetImage, $foregroundColor['r'], $foregroundColor['g'], $foregroundColor['b']);
$backgroundColor = imagecolorallocate($targetImage, $backgroundColor['r'], $backgroundColor['g'], $backgroundColor['b']);
imagefill($targetImage, 0, 0, $backgroundColor);
// Copy source image to target image
imagecopyresampled($targetImage, $sourceImage, 0, 0, 0, 0, $sourceWidth, $sourceHeight, $sourceWidth, $sourceHeight);
imagedestroy($sourceImage);
switch ($labelAlignment) {
case LabelAlignment::LEFT:
$labelX = $labelMargin['l'];
break;
case LabelAlignment::RIGHT:
$labelX = $targetWidth - $labelBoxWidth - $labelMargin['r'];
break;
default:
$labelX = intval($targetWidth / 2 - $labelBoxWidth / 2);
break;
}
$labelY = $targetHeight - $labelMargin['b'];
imagettftext($targetImage, $labelFontSize, 0, $labelX, $labelY, $foregroundColor, $labelFontPath, $label);
return $targetImage;
}
private function imageToString($image): string
{
ob_start();
imagepng($image);
return (string) ob_get_clean();
}
public static function getContentType(): string
{
return 'image/png';
}
public static function getSupportedExtensions(): array
{
return ['png'];
}
public function getName(): string
{
return 'png';
}
} }

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
abstract class AbstractResult implements ResultInterface
{
public function getDataUri(): string
{
return 'data:'.$this->getMimeType().';base64,'.base64_encode($this->getString());
}
public function saveToFile(string $path): void
{
$string = $this->getString();
file_put_contents($path, $string);
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
use Endroid\QrCode\Matrix\MatrixInterface;
final class BinaryResult extends AbstractResult
{
private MatrixInterface $matrix;
public function __construct(MatrixInterface $matrix)
{
$this->matrix = $matrix;
}
public function getString(): string
{
$binaryString = '';
for ($rowIndex = 0; $rowIndex < $this->matrix->getBlockCount(); ++$rowIndex) {
for ($columnIndex = 0; $columnIndex < $this->matrix->getBlockCount(); ++$columnIndex) {
$binaryString .= $this->matrix->getBlockValue($rowIndex, $columnIndex);
}
$binaryString .= "\n";
}
return $binaryString;
}
public function getMimeType(): string
{
return 'text/plain';
}
}

View File

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface;
final class DebugResult extends AbstractResult
{
private QrCodeInterface $qrCode;
private ?LogoInterface $logo;
private ?LabelInterface $label;
/** @var array<mixed> */
private array $options;
private bool $validateResult = false;
/** @param array<mixed> $options */
public function __construct(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = [])
{
$this->qrCode = $qrCode;
$this->logo = $logo;
$this->label = $label;
$this->options = $options;
}
public function setValidateResult(bool $validateResult): void
{
$this->validateResult = $validateResult;
}
public function getString(): string
{
$debugLines = [];
$debugLines[] = 'Data: '.$this->qrCode->getData();
$debugLines[] = 'Encoding: '.$this->qrCode->getEncoding();
$debugLines[] = 'Error Correction Level: '.get_class($this->qrCode->getErrorCorrectionLevel());
$debugLines[] = 'Size: '.$this->qrCode->getSize();
$debugLines[] = 'Margin: '.$this->qrCode->getMargin();
$debugLines[] = 'Round block size mode: '.get_class($this->qrCode->getRoundBlockSizeMode());
$debugLines[] = 'Foreground color: ['.implode(', ', $this->qrCode->getForegroundColor()->toArray()).']';
$debugLines[] = 'Background color: ['.implode(', ', $this->qrCode->getBackgroundColor()->toArray()).']';
foreach ($this->options as $key => $value) {
$debugLines[] = 'Writer option: '.$key.': '.$value;
}
if (isset($this->logo)) {
$debugLines[] = 'Logo path: '.$this->logo->getPath();
$debugLines[] = 'Logo resize to width: '.$this->logo->getResizeToWidth();
$debugLines[] = 'Logo resize to height: '.$this->logo->getResizeToHeight();
}
if (isset($this->label)) {
$debugLines[] = 'Label text: '.$this->label->getText();
$debugLines[] = 'Label font path: '.$this->label->getFont()->getPath();
$debugLines[] = 'Label font size: '.$this->label->getFont()->getSize();
$debugLines[] = 'Label alignment: '.get_class($this->label->getAlignment());
$debugLines[] = 'Label margin: ['.implode(', ', $this->label->getMargin()->toArray()).']';
$debugLines[] = 'Label text color: ['.implode(', ', $this->label->getTextColor()->toArray()).']';
}
$debugLines[] = 'Validate result: '.($this->validateResult ? 'true' : 'false');
return implode("\n", $debugLines);
}
public function getMimeType(): string
{
return 'text/plain';
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
final class EpsResult extends AbstractResult
{
/** @var array<string> */
private array $lines;
/** @param array<string> $lines */
public function __construct(array $lines)
{
$this->lines = $lines;
}
public function getString(): string
{
return implode("\n", $this->lines);
}
public function getMimeType(): string
{
return 'image/eps';
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
final class PdfResult extends AbstractResult
{
private \FPDF $fpdf;
public function __construct(\FPDF $fpdf)
{
$this->fpdf = $fpdf;
}
public function getPdf(): \FPDF
{
return $this->fpdf;
}
public function getString(): string
{
return $this->fpdf->Output('S');
}
public function getMimeType(): string
{
return 'application/pdf';
}
}

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
final class PngResult extends AbstractResult
{
/** @var mixed */
private $image;
/** @param mixed $image */
public function __construct($image)
{
$this->image = $image;
}
/** @return mixed */
public function getImage()
{
return $this->image;
}
public function getString(): string
{
ob_start();
imagepng($this->image);
return strval(ob_get_clean());
}
public function getMimeType(): string
{
return 'image/png';
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
interface ResultInterface
{
public function getString(): string;
public function getDataUri(): string;
public function saveToFile(string $path): void;
public function getMimeType(): string;
}

View File

@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer\Result;
final class SvgResult extends AbstractResult
{
private \SimpleXMLElement $xml;
private bool $excludeXmlDeclaration;
public function __construct(\SimpleXMLElement $xml, bool $excludeXmlDeclaration = false)
{
$this->xml = $xml;
$this->excludeXmlDeclaration = $excludeXmlDeclaration;
}
public function getXml(): \SimpleXMLElement
{
return $this->xml;
}
public function getString(): string
{
$string = $this->xml->asXML();
if (!is_string($string)) {
throw new \Exception('Could not save SVG XML to string');
}
if ($this->excludeXmlDeclaration) {
$string = str_replace("<?xml version=\"1.0\"?>\n", '', $string);
}
return $string;
}
public function getMimeType(): string
{
return 'image/svg+xml';
}
}

View File

@ -2,151 +2,106 @@
declare(strict_types=1); declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer; namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Exception\GenerateImageException; use Endroid\QrCode\Bacon\MatrixFactory;
use Endroid\QrCode\Exception\MissingLogoHeightException; use Endroid\QrCode\ImageData\LogoImageData;
use Endroid\QrCode\Exception\ValidationException; use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface; use Endroid\QrCode\QrCodeInterface;
use SimpleXMLElement; use Endroid\QrCode\Writer\Result\ResultInterface;
use Endroid\QrCode\Writer\Result\SvgResult;
class SvgWriter extends AbstractWriter final class SvgWriter implements WriterInterface
{ {
public function writeString(QrCodeInterface $qrCode): string public const DECIMAL_PRECISION = 10;
public const WRITER_OPTION_BLOCK_ID = 'block_id';
public const WRITER_OPTION_EXCLUDE_XML_DECLARATION = 'exclude_xml_declaration';
public const WRITER_OPTION_FORCE_XLINK_HREF = 'force_xlink_href';
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface
{ {
if ($qrCode->getValidateResult()) { if (!isset($options[self::WRITER_OPTION_BLOCK_ID])) {
throw new ValidationException('Built-in validation reader can not check SVG images: please disable via setValidateResult(false)'); $options[self::WRITER_OPTION_BLOCK_ID] = 'block';
} }
$data = $qrCode->getData(); if (!isset($options[self::WRITER_OPTION_EXCLUDE_XML_DECLARATION])) {
$options[self::WRITER_OPTION_EXCLUDE_XML_DECLARATION] = false;
}
$svg = new SimpleXMLElement('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>'); $matrixFactory = new MatrixFactory();
$svg->addAttribute('version', '1.1'); $matrix = $matrixFactory->create($qrCode);
$svg->addAttribute('width', $data['outer_width'].'px');
$svg->addAttribute('height', $data['outer_height'].'px');
$svg->addAttribute('viewBox', '0 0 '.$data['outer_width'].' '.$data['outer_height']);
$svg->addChild('defs');
// Block definition $xml = new \SimpleXMLElement('<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"/>');
$blockDefinition = $svg->defs->addChild('rect'); $xml->addAttribute('version', '1.1');
$blockDefinition->addAttribute('id', 'block'); $xml->addAttribute('width', $matrix->getOuterSize().'px');
$blockDefinition->addAttribute('width', strval($data['block_size'])); $xml->addAttribute('height', $matrix->getOuterSize().'px');
$blockDefinition->addAttribute('height', strval($data['block_size'])); $xml->addAttribute('viewBox', '0 0 '.$matrix->getOuterSize().' '.$matrix->getOuterSize());
$blockDefinition->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getForegroundColor()['r'], $qrCode->getForegroundColor()['g'], $qrCode->getForegroundColor()['b'])); $xml->addChild('defs');
$blockDefinition->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getForegroundColor()['a'])));
// Background $blockDefinition = $xml->defs->addChild('rect');
$background = $svg->addChild('rect'); $blockDefinition->addAttribute('id', strval($options[self::WRITER_OPTION_BLOCK_ID]));
$blockDefinition->addAttribute('width', number_format($matrix->getBlockSize(), self::DECIMAL_PRECISION, '.', ''));
$blockDefinition->addAttribute('height', number_format($matrix->getBlockSize(), self::DECIMAL_PRECISION, '.', ''));
$blockDefinition->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getForegroundColor()->getRed(), $qrCode->getForegroundColor()->getGreen(), $qrCode->getForegroundColor()->getBlue()));
$blockDefinition->addAttribute('fill-opacity', strval($qrCode->getForegroundColor()->getOpacity()));
$background = $xml->addChild('rect');
$background->addAttribute('x', '0'); $background->addAttribute('x', '0');
$background->addAttribute('y', '0'); $background->addAttribute('y', '0');
$background->addAttribute('width', strval($data['outer_width'])); $background->addAttribute('width', strval($matrix->getOuterSize()));
$background->addAttribute('height', strval($data['outer_height'])); $background->addAttribute('height', strval($matrix->getOuterSize()));
$background->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getBackgroundColor()['r'], $qrCode->getBackgroundColor()['g'], $qrCode->getBackgroundColor()['b'])); $background->addAttribute('fill', '#'.sprintf('%02x%02x%02x', $qrCode->getBackgroundColor()->getRed(), $qrCode->getBackgroundColor()->getGreen(), $qrCode->getBackgroundColor()->getBlue()));
$background->addAttribute('fill-opacity', strval($this->getOpacity($qrCode->getBackgroundColor()['a']))); $background->addAttribute('fill-opacity', strval($qrCode->getBackgroundColor()->getOpacity()));
foreach ($data['matrix'] as $row => $values) { for ($rowIndex = 0; $rowIndex < $matrix->getBlockCount(); ++$rowIndex) {
foreach ($values as $column => $value) { for ($columnIndex = 0; $columnIndex < $matrix->getBlockCount(); ++$columnIndex) {
if (1 === $value) { if (1 === $matrix->getBlockValue($rowIndex, $columnIndex)) {
$block = $svg->addChild('use'); $block = $xml->addChild('use');
$block->addAttribute('x', strval($data['margin_left'] + $data['block_size'] * $column)); $block->addAttribute('x', number_format($matrix->getMarginLeft() + $matrix->getBlockSize() * $columnIndex, self::DECIMAL_PRECISION, '.', ''));
$block->addAttribute('y', strval($data['margin_left'] + $data['block_size'] * $row)); $block->addAttribute('y', number_format($matrix->getMarginLeft() + $matrix->getBlockSize() * $rowIndex, self::DECIMAL_PRECISION, '.', ''));
$block->addAttribute('xlink:href', '#block', 'http://www.w3.org/1999/xlink'); $block->addAttribute('xlink:href', '#'.$options[self::WRITER_OPTION_BLOCK_ID], 'http://www.w3.org/1999/xlink');
} }
} }
} }
$logoPath = $qrCode->getLogoPath(); $result = new SvgResult($xml, boolval($options[self::WRITER_OPTION_EXCLUDE_XML_DECLARATION]));
if (is_string($logoPath)) {
$this->addLogo($svg, $data['outer_width'], $data['outer_height'], $logoPath, $qrCode->getLogoWidth(), $qrCode->getLogoHeight()); if ($logo instanceof LogoInterface) {
$this->addLogo($logo, $result, $options);
} }
$xml = $svg->asXML(); return $result;
if (!is_string($xml)) {
throw new GenerateImageException('Unable to save SVG XML');
} }
$options = $qrCode->getWriterOptions(); /** @param array<mixed> $options */
if (isset($options['exclude_xml_declaration']) && $options['exclude_xml_declaration']) { private function addLogo(LogoInterface $logo, SvgResult $result, array $options): void
$xml = str_replace("<?xml version=\"1.0\"?>\n", '', $xml);
}
return $xml;
}
private function addLogo(SimpleXMLElement $svg, int $imageWidth, int $imageHeight, string $logoPath, int $logoWidth = null, int $logoHeight = null): void
{ {
$mimeType = $this->getMimeType($logoPath); $logoImageData = LogoImageData::createForLogo($logo);
$imageData = file_get_contents($logoPath);
if (!is_string($imageData)) { if (!isset($options[self::WRITER_OPTION_FORCE_XLINK_HREF])) {
throw new GenerateImageException('Unable to read image data: check your logo path'); $options[self::WRITER_OPTION_FORCE_XLINK_HREF] = false;
} }
if ('image/svg+xml' === $mimeType && (null === $logoHeight || null === $logoWidth)) { $xml = $result->getXml();
throw new MissingLogoHeightException('SVG Logos require an explicit height set via setLogoSize($width, $height)');
}
if (null === $logoHeight || null === $logoWidth) { /** @var \SimpleXMLElement $xmlAttributes */
$logoImage = imagecreatefromstring(strval($imageData)); $xmlAttributes = $xml->attributes();
if (!is_resource($logoImage)) { $x = intval($xmlAttributes->width) / 2 - $logoImageData->getWidth() / 2;
throw new GenerateImageException('Unable to generate image: check your GD installation or logo path'); $y = intval($xmlAttributes->height) / 2 - $logoImageData->getHeight() / 2;
}
$logoSourceWidth = imagesx($logoImage); $imageDefinition = $xml->addChild('image');
$logoSourceHeight = imagesy($logoImage); $imageDefinition->addAttribute('x', strval($x));
$imageDefinition->addAttribute('y', strval($y));
imagedestroy($logoImage); $imageDefinition->addAttribute('width', strval($logoImageData->getWidth()));
$imageDefinition->addAttribute('height', strval($logoImageData->getHeight()));
if (null === $logoWidth) {
$logoWidth = $logoSourceWidth;
}
if (null === $logoHeight) {
$aspectRatio = $logoWidth / $logoSourceWidth;
$logoHeight = intval($logoSourceHeight * $aspectRatio);
}
}
$logoX = $imageWidth / 2 - $logoWidth / 2;
$logoY = $imageHeight / 2 - $logoHeight / 2;
$imageDefinition = $svg->addChild('image');
$imageDefinition->addAttribute('x', strval($logoX));
$imageDefinition->addAttribute('y', strval($logoY));
$imageDefinition->addAttribute('width', strval($logoWidth));
$imageDefinition->addAttribute('height', strval($logoHeight));
$imageDefinition->addAttribute('preserveAspectRatio', 'none'); $imageDefinition->addAttribute('preserveAspectRatio', 'none');
$imageDefinition->addAttribute('xlink:href', 'data:'.$mimeType.';base64,'.base64_encode($imageData));
}
private function getOpacity(int $alpha): float if ($options[self::WRITER_OPTION_FORCE_XLINK_HREF]) {
{ $imageDefinition->addAttribute('xlink:href', $logoImageData->createDataUri(), 'http://www.w3.org/1999/xlink');
$opacity = 1 - $alpha / 127; } else {
$imageDefinition->addAttribute('href', $logoImageData->createDataUri());
return $opacity; }
}
public static function getContentType(): string
{
return 'image/svg+xml';
}
public static function getSupportedExtensions(): array
{
return ['svg'];
}
public function getName(): string
{
return 'svg';
} }
} }

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Writer\Result\ResultInterface;
interface ValidatingWriterInterface
{
public function validateResult(ResultInterface $result, string $expectedData): void;
}

View File

@ -2,30 +2,15 @@
declare(strict_types=1); declare(strict_types=1);
/*
* (c) Jeroen van den Enden <info@endroid.nl>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/
namespace Endroid\QrCode\Writer; namespace Endroid\QrCode\Writer;
use Endroid\QrCode\Label\LabelInterface;
use Endroid\QrCode\Logo\LogoInterface;
use Endroid\QrCode\QrCodeInterface; use Endroid\QrCode\QrCodeInterface;
use Endroid\QrCode\Writer\Result\ResultInterface;
interface WriterInterface interface WriterInterface
{ {
public function writeString(QrCodeInterface $qrCode): string; /** @param array<mixed> $options */
public function write(QrCodeInterface $qrCode, LogoInterface $logo = null, LabelInterface $label = null, array $options = []): ResultInterface;
public function writeDataUri(QrCodeInterface $qrCode): string;
public function writeFile(QrCodeInterface $qrCode, string $path): void;
public static function getContentType(): string;
public static function supportsExtension(string $extension): bool;
public static function getSupportedExtensions(): array;
public function getName(): string;
} }

23
vendor/guzzlehttp/guzzle/.php_cs vendored Normal file
View File

@ -0,0 +1,23 @@
<?php
$config = PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'declare_strict_types' => false,
'concat_space' => ['spacing'=>'one'],
'php_unit_test_case_static_method_calls' => ['call_type' => 'self'],
'ordered_imports' => true,
// 'phpdoc_align' => ['align'=>'vertical'],
// 'native_function_invocation' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
->name('*.php')
)
;
return $config;

18
vendor/guzzlehttp/guzzle/Dockerfile vendored Normal file
View File

@ -0,0 +1,18 @@
FROM composer:latest as setup
RUN mkdir /guzzle
WORKDIR /guzzle
RUN set -xe \
&& composer init --name=guzzlehttp/test --description="Simple project for testing Guzzle scripts" --author="Márk Sági-Kazár <mark.sagikazar@gmail.com>" --no-interaction \
&& composer require guzzlehttp/guzzle
FROM php:7.3
RUN mkdir /guzzle
WORKDIR /guzzle
COPY --from=setup /guzzle /guzzle

View File

@ -0,0 +1,7 @@
<?php
namespace GuzzleHttp\Exception;
final class InvalidArgumentException extends \InvalidArgumentException implements GuzzleException
{
}

View File

@ -1,10 +1,9 @@
<?php <?php
namespace GuzzleHttp\Handler; namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\FulfilledPromise; use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Psr7; use GuzzleHttp\Psr7;
use GuzzleHttp\Psr7\LazyOpenStream; use GuzzleHttp\Psr7\LazyOpenStream;
use GuzzleHttp\TransferStats; use GuzzleHttp\TransferStats;
@ -15,6 +14,9 @@ use Psr\Http\Message\RequestInterface;
*/ */
class CurlFactory implements CurlFactoryInterface class CurlFactory implements CurlFactoryInterface
{ {
const CURL_VERSION_STR = 'curl_version';
const LOW_CURL_VERSION_NUMBER = '7.21.2';
/** @var array */ /** @var array */
private $handles = []; private $handles = [];
@ -118,6 +120,7 @@ class CurlFactory implements CurlFactoryInterface
private static function invokeStats(EasyHandle $easy) private static function invokeStats(EasyHandle $easy)
{ {
$curlStats = curl_getinfo($easy->handle); $curlStats = curl_getinfo($easy->handle);
$curlStats['appconnect_time'] = curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME);
$stats = new TransferStats( $stats = new TransferStats(
$easy->request, $easy->request,
$easy->response, $easy->response,
@ -137,7 +140,9 @@ class CurlFactory implements CurlFactoryInterface
$ctx = [ $ctx = [
'errno' => $easy->errno, 'errno' => $easy->errno,
'error' => curl_error($easy->handle), 'error' => curl_error($easy->handle),
'appconnect_time' => curl_getinfo($easy->handle, CURLINFO_APPCONNECT_TIME),
] + curl_getinfo($easy->handle); ] + curl_getinfo($easy->handle);
$ctx[self::CURL_VERSION_STR] = curl_version()['version'];
$factory->release($easy); $factory->release($easy);
// Retry when nothing is present or when curl failed to rewind. // Retry when nothing is present or when curl failed to rewind.
@ -173,13 +178,22 @@ class CurlFactory implements CurlFactoryInterface
) )
); );
} }
if (version_compare($ctx[self::CURL_VERSION_STR], self::LOW_CURL_VERSION_NUMBER)) {
$message = sprintf( $message = sprintf(
'cURL error %s: %s (%s)', 'cURL error %s: %s (%s)',
$ctx['errno'], $ctx['errno'],
$ctx['error'], $ctx['error'],
'see http://curl.haxx.se/libcurl/c/libcurl-errors.html' 'see https://curl.haxx.se/libcurl/c/libcurl-errors.html'
); );
} else {
$message = sprintf(
'cURL error %s: %s (%s) for %s',
$ctx['errno'],
$ctx['error'],
'see https://curl.haxx.se/libcurl/c/libcurl-errors.html',
$easy->request->getUri()
);
}
// Create a connection exception if it was a specific error code. // Create a connection exception if it was a specific error code.
$error = isset($connectionErrors[$easy->errno]) $error = isset($connectionErrors[$easy->errno])
@ -288,9 +302,16 @@ class CurlFactory implements CurlFactoryInterface
{ {
foreach ($conf['_headers'] as $name => $values) { foreach ($conf['_headers'] as $name => $values) {
foreach ($values as $value) { foreach ($values as $value) {
$value = (string) $value;
if ($value === '') {
// cURL requires a special format for empty headers.
// See https://github.com/guzzle/guzzle/issues/1882 for more details.
$conf[CURLOPT_HTTPHEADER][] = "$name;";
} else {
$conf[CURLOPT_HTTPHEADER][] = "$name: $value"; $conf[CURLOPT_HTTPHEADER][] = "$name: $value";
} }
} }
}
// Remove the Accept header if one was not set // Remove the Accept header if one was not set
if (!$easy->request->hasHeader('Accept')) { if (!$easy->request->hasHeader('Accept')) {
@ -433,11 +454,16 @@ class CurlFactory implements CurlFactoryInterface
} }
if (isset($options['ssl_key'])) { if (isset($options['ssl_key'])) {
$sslKey = $options['ssl_key']; if (is_array($options['ssl_key'])) {
if (is_array($sslKey)) { if (count($options['ssl_key']) === 2) {
$conf[CURLOPT_SSLKEYPASSWD] = $sslKey[1]; list($sslKey, $conf[CURLOPT_SSLKEYPASSWD]) = $options['ssl_key'];
$sslKey = $sslKey[0]; } else {
list($sslKey) = $options['ssl_key'];
} }
}
$sslKey = isset($sslKey) ? $sslKey: $options['ssl_key'];
if (!file_exists($sslKey)) { if (!file_exists($sslKey)) {
throw new \InvalidArgumentException( throw new \InvalidArgumentException(
"SSL private key not found: {$sslKey}" "SSL private key not found: {$sslKey}"

View File

@ -3,7 +3,7 @@ namespace GuzzleHttp\Handler;
use GuzzleHttp\Promise as P; use GuzzleHttp\Promise as P;
use GuzzleHttp\Promise\Promise; use GuzzleHttp\Promise\Promise;
use GuzzleHttp\Psr7; use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
/** /**
@ -23,6 +23,7 @@ class CurlMultiHandler
private $active; private $active;
private $handles = []; private $handles = [];
private $delays = []; private $delays = [];
private $options = [];
/** /**
* This handler accepts the following options: * This handler accepts the following options:
@ -30,6 +31,8 @@ class CurlMultiHandler
* - handle_factory: An optional factory used to create curl handles * - handle_factory: An optional factory used to create curl handles
* - select_timeout: Optional timeout (in seconds) to block before timing * - select_timeout: Optional timeout (in seconds) to block before timing
* out while selecting curl handles. Defaults to 1 second. * out while selecting curl handles. Defaults to 1 second.
* - options: An associative array of CURLMOPT_* options and
* corresponding values for curl_multi_setopt()
* *
* @param array $options * @param array $options
*/ */
@ -37,14 +40,31 @@ class CurlMultiHandler
{ {
$this->factory = isset($options['handle_factory']) $this->factory = isset($options['handle_factory'])
? $options['handle_factory'] : new CurlFactory(50); ? $options['handle_factory'] : new CurlFactory(50);
$this->selectTimeout = isset($options['select_timeout'])
? $options['select_timeout'] : 1; if (isset($options['select_timeout'])) {
$this->selectTimeout = $options['select_timeout'];
} elseif ($selectTimeout = getenv('GUZZLE_CURL_SELECT_TIMEOUT')) {
$this->selectTimeout = $selectTimeout;
} else {
$this->selectTimeout = 1;
}
$this->options = isset($options['options']) ? $options['options'] : [];
} }
public function __get($name) public function __get($name)
{ {
if ($name === '_mh') { if ($name === '_mh') {
return $this->_mh = curl_multi_init(); $this->_mh = curl_multi_init();
foreach ($this->options as $option => $value) {
// A warning is raised in case of a wrong option.
curl_multi_setopt($this->_mh, $option, $value);
}
// Further calls to _mh will return the value directly, without entering the
// __get() method at all.
return $this->_mh;
} }
throw new \BadMethodCallException(); throw new \BadMethodCallException();
@ -65,7 +85,9 @@ class CurlMultiHandler
$promise = new Promise( $promise = new Promise(
[$this, 'execute'], [$this, 'execute'],
function () use ($id) { return $this->cancel($id); } function () use ($id) {
return $this->cancel($id);
}
); );
$this->addRequest(['easy' => $easy, 'deferred' => $promise]); $this->addRequest(['easy' => $easy, 'deferred' => $promise]);
@ -80,7 +102,7 @@ class CurlMultiHandler
{ {
// Add any delayed handles if needed. // Add any delayed handles if needed.
if ($this->delays) { if ($this->delays) {
$currentTime = microtime(true); $currentTime = Utils::currentTime();
foreach ($this->delays as $id => $delay) { foreach ($this->delays as $id => $delay) {
if ($currentTime >= $delay) { if ($currentTime >= $delay) {
unset($this->delays[$id]); unset($this->delays[$id]);
@ -132,7 +154,7 @@ class CurlMultiHandler
if (empty($easy->options['delay'])) { if (empty($easy->options['delay'])) {
curl_multi_add_handle($this->_mh, $easy->handle); curl_multi_add_handle($this->_mh, $easy->handle);
} else { } else {
$this->delays[$id] = microtime(true) + ($easy->options['delay'] / 1000); $this->delays[$id] = Utils::currentTime() + ($easy->options['delay'] / 1000);
} }
} }
@ -184,7 +206,7 @@ class CurlMultiHandler
private function timeToNext() private function timeToNext()
{ {
$currentTime = microtime(true); $currentTime = Utils::currentTime();
$nextTime = PHP_INT_MAX; $nextTime = PHP_INT_MAX;
foreach ($this->delays as $time) { foreach ($this->delays as $time) {
if ($time < $nextTime) { if ($time < $nextTime) {

View File

@ -66,7 +66,7 @@ class MockHandler implements \Countable
throw new \OutOfBoundsException('Mock queue is empty'); throw new \OutOfBoundsException('Mock queue is empty');
} }
if (isset($options['delay'])) { if (isset($options['delay']) && is_numeric($options['delay'])) {
usleep($options['delay'] * 1000); usleep($options['delay'] * 1000);
} }
@ -175,6 +175,11 @@ class MockHandler implements \Countable
return count($this->queue); return count($this->queue);
} }
public function reset()
{
$this->queue = [];
}
private function invokeStats( private function invokeStats(
RequestInterface $request, RequestInterface $request,
array $options, array $options,
@ -182,7 +187,8 @@ class MockHandler implements \Countable
$reason = null $reason = null
) { ) {
if (isset($options['on_stats'])) { if (isset($options['on_stats'])) {
$stats = new TransferStats($request, $response, 0, $reason); $transferTime = isset($options['transfer_time']) ? $options['transfer_time'] : 0;
$stats = new TransferStats($request, $response, $transferTime, $reason);
call_user_func($options['on_stats'], $stats); call_user_func($options['on_stats'], $stats);
} }
} }

View File

@ -1,13 +1,13 @@
<?php <?php
namespace GuzzleHttp\Handler; namespace GuzzleHttp\Handler;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\FulfilledPromise; use GuzzleHttp\Promise\FulfilledPromise;
use GuzzleHttp\Promise\RejectedPromise;
use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Psr7; use GuzzleHttp\Psr7;
use GuzzleHttp\TransferStats; use GuzzleHttp\TransferStats;
use GuzzleHttp\Utils;
use Psr\Http\Message\RequestInterface; use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamInterface; use Psr\Http\Message\StreamInterface;
@ -34,7 +34,7 @@ class StreamHandler
usleep($options['delay'] * 1000); usleep($options['delay'] * 1000);
} }
$startTime = isset($options['on_stats']) ? microtime(true) : null; $startTime = isset($options['on_stats']) ? Utils::currentTime() : null;
try { try {
// Does not support the expect header. // Does not support the expect header.
@ -43,7 +43,7 @@ class StreamHandler
// Append a content-length header if body size is zero to match // Append a content-length header if body size is zero to match
// cURL's behavior. // cURL's behavior.
if (0 === $request->getBody()->getSize()) { if (0 === $request->getBody()->getSize()) {
$request = $request->withHeader('Content-Length', 0); $request = $request->withHeader('Content-Length', '0');
} }
return $this->createResponse( return $this->createResponse(
@ -61,6 +61,7 @@ class StreamHandler
if (strpos($message, 'getaddrinfo') // DNS lookup failed if (strpos($message, 'getaddrinfo') // DNS lookup failed
|| strpos($message, 'Connection refused') || strpos($message, 'Connection refused')
|| strpos($message, "couldn't connect to host") // error on HHVM || strpos($message, "couldn't connect to host") // error on HHVM
|| strpos($message, "connection attempt failed")
) { ) {
$e = new ConnectException($e->getMessage(), $request, $e); $e = new ConnectException($e->getMessage(), $request, $e);
} }
@ -82,7 +83,7 @@ class StreamHandler
$stats = new TransferStats( $stats = new TransferStats(
$request, $request,
$response, $response,
microtime(true) - $startTime, Utils::currentTime() - $startTime,
$error, $error,
[] []
); );
@ -276,7 +277,7 @@ class StreamHandler
} }
$params = []; $params = [];
$context = $this->getDefaultContext($request, $options); $context = $this->getDefaultContext($request);
if (isset($options['on_headers']) && !is_callable($options['on_headers'])) { if (isset($options['on_headers']) && !is_callable($options['on_headers'])) {
throw new \InvalidArgumentException('on_headers must be callable'); throw new \InvalidArgumentException('on_headers must be callable');
@ -307,7 +308,6 @@ class StreamHandler
&& isset($options['auth'][2]) && isset($options['auth'][2])
&& 'ntlm' == $options['auth'][2] && 'ntlm' == $options['auth'][2]
) { ) {
throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler'); throw new \InvalidArgumentException('Microsoft NTLM authentication only supported with curl handler');
} }
@ -344,13 +344,25 @@ class StreamHandler
if ('v4' === $options['force_ip_resolve']) { if ('v4' === $options['force_ip_resolve']) {
$records = dns_get_record($uri->getHost(), DNS_A); $records = dns_get_record($uri->getHost(), DNS_A);
if (!isset($records[0]['ip'])) { if (!isset($records[0]['ip'])) {
throw new ConnectException(sprintf("Could not resolve IPv4 address for host '%s'", $uri->getHost()), $request); throw new ConnectException(
sprintf(
"Could not resolve IPv4 address for host '%s'",
$uri->getHost()
),
$request
);
} }
$uri = $uri->withHost($records[0]['ip']); $uri = $uri->withHost($records[0]['ip']);
} elseif ('v6' === $options['force_ip_resolve']) { } elseif ('v6' === $options['force_ip_resolve']) {
$records = dns_get_record($uri->getHost(), DNS_AAAA); $records = dns_get_record($uri->getHost(), DNS_AAAA);
if (!isset($records[0]['ipv6'])) { if (!isset($records[0]['ipv6'])) {
throw new ConnectException(sprintf("Could not resolve IPv6 address for host '%s'", $uri->getHost()), $request); throw new ConnectException(
sprintf(
"Could not resolve IPv6 address for host '%s'",
$uri->getHost()
),
$request
);
} }
$uri = $uri->withHost('[' . $records[0]['ipv6'] . ']'); $uri = $uri->withHost('[' . $records[0]['ipv6'] . ']');
} }

92
vendor/guzzlehttp/guzzle/src/Utils.php vendored Normal file
View File

@ -0,0 +1,92 @@
<?php
namespace GuzzleHttp;
use GuzzleHttp\Exception\InvalidArgumentException;
use Psr\Http\Message\UriInterface;
use Symfony\Polyfill\Intl\Idn\Idn;
final class Utils
{
/**
* Wrapper for the hrtime() or microtime() functions
* (depending on the PHP version, one of the two is used)
*
* @return float|mixed UNIX timestamp
*
* @internal
*/
public static function currentTime()
{
return function_exists('hrtime') ? hrtime(true) / 1e9 : microtime(true);
}
/**
* @param int $options
*
* @return UriInterface
* @throws InvalidArgumentException
*
* @internal
*/
public static function idnUriConvert(UriInterface $uri, $options = 0)
{
if ($uri->getHost()) {
$asciiHost = self::idnToAsci($uri->getHost(), $options, $info);
if ($asciiHost === false) {
$errorBitSet = isset($info['errors']) ? $info['errors'] : 0;
$errorConstants = array_filter(array_keys(get_defined_constants()), function ($name) {
return substr($name, 0, 11) === 'IDNA_ERROR_';
});
$errors = [];
foreach ($errorConstants as $errorConstant) {
if ($errorBitSet & constant($errorConstant)) {
$errors[] = $errorConstant;
}
}
$errorMessage = 'IDN conversion failed';
if ($errors) {
$errorMessage .= ' (errors: ' . implode(', ', $errors) . ')';
}
throw new InvalidArgumentException($errorMessage);
} else {
if ($uri->getHost() !== $asciiHost) {
// Replace URI only if the ASCII version is different
$uri = $uri->withHost($asciiHost);
}
}
}
return $uri;
}
/**
* @param string $domain
* @param int $options
* @param array $info
*
* @return string|false
*/
private static function idnToAsci($domain, $options, &$info = [])
{
if (\preg_match('%^[ -~]+$%', $domain) === 1) {
return $domain;
}
if (\extension_loaded('intl') && defined('INTL_IDNA_VARIANT_UTS46')) {
return \idn_to_ascii($domain, $options, INTL_IDNA_VARIANT_UTS46, $info);
}
/*
* The Idn class is marked as @internal. Verify that class and method exists.
*/
if (method_exists(Idn::class, 'idn_to_ascii')) {
return Idn::idn_to_ascii($domain, $options, Idn::INTL_IDNA_VARIANT_UTS46, $info);
}
throw new \RuntimeException('ext-intl or symfony/polyfill-intl-idn not loaded or too old');
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace GuzzleHttp\Promise;
final class Create
{
/**
* Creates a promise for a value if the value is not a promise.
*
* @param mixed $value Promise or value.
*
* @return PromiseInterface
*/
public static function promiseFor($value)
{
if ($value instanceof PromiseInterface) {
return $value;
}
// Return a Guzzle promise that shadows the given promise.
if (is_object($value) && method_exists($value, 'then')) {
$wfn = method_exists($value, 'wait') ? [$value, 'wait'] : null;
$cfn = method_exists($value, 'cancel') ? [$value, 'cancel'] : null;
$promise = new Promise($wfn, $cfn);
$value->then([$promise, 'resolve'], [$promise, 'reject']);
return $promise;
}
return new FulfilledPromise($value);
}
/**
* Creates a rejected promise for a reason if the reason is not a promise.
* If the provided reason is a promise, then it is returned as-is.
*
* @param mixed $reason Promise or reason.
*
* @return PromiseInterface
*/
public static function rejectionFor($reason)
{
if ($reason instanceof PromiseInterface) {
return $reason;
}
return new RejectedPromise($reason);
}
/**
* Create an exception for a rejected promise value.
*
* @param mixed $reason
*
* @return \Exception|\Throwable
*/
public static function exceptionFor($reason)
{
if ($reason instanceof \Exception || $reason instanceof \Throwable) {
return $reason;
}
return new RejectionException($reason);
}
/**
* Returns an iterator for the given value.
*
* @param mixed $value
*
* @return \Iterator
*/
public static function iterFor($value)
{
if ($value instanceof \Iterator) {
return $value;
}
if (is_array($value)) {
return new \ArrayIterator($value);
}
return new \ArrayIterator([$value]);
}
}

90
vendor/guzzlehttp/promises/src/Each.php vendored Normal file
View File

@ -0,0 +1,90 @@
<?php
namespace GuzzleHttp\Promise;
final class Each
{
/**
* Given an iterator that yields promises or values, returns a promise that
* is fulfilled with a null value when the iterator has been consumed or
* the aggregate promise has been fulfilled or rejected.
*
* $onFulfilled is a function that accepts the fulfilled value, iterator
* index, and the aggregate promise. The callback can invoke any necessary
* side effects and choose to resolve or reject the aggregate if needed.
*
* $onRejected is a function that accepts the rejection reason, iterator
* index, and the aggregate promise. The callback can invoke any necessary
* side effects and choose to resolve or reject the aggregate if needed.
*
* @param mixed $iterable Iterator or array to iterate over.
* @param callable $onFulfilled
* @param callable $onRejected
*
* @return PromiseInterface
*/
public static function of(
$iterable,
callable $onFulfilled = null,
callable $onRejected = null
) {
return (new EachPromise($iterable, [
'fulfilled' => $onFulfilled,
'rejected' => $onRejected
]))->promise();
}
/**
* Like of, but only allows a certain number of outstanding promises at any
* given time.
*
* $concurrency may be an integer or a function that accepts the number of
* pending promises and returns a numeric concurrency limit value to allow
* for dynamic a concurrency size.
*
* @param mixed $iterable
* @param int|callable $concurrency
* @param callable $onFulfilled
* @param callable $onRejected
*
* @return PromiseInterface
*/
public static function ofLimit(
$iterable,
$concurrency,
callable $onFulfilled = null,
callable $onRejected = null
) {
return (new EachPromise($iterable, [
'fulfilled' => $onFulfilled,
'rejected' => $onRejected,
'concurrency' => $concurrency
]))->promise();
}
/**
* Like limit, but ensures that no promise in the given $iterable argument
* is rejected. If any promise is rejected, then the aggregate promise is
* rejected with the encountered rejection.
*
* @param mixed $iterable
* @param int|callable $concurrency
* @param callable $onFulfilled
*
* @return PromiseInterface
*/
public static function ofLimitAll(
$iterable,
$concurrency,
callable $onFulfilled = null
) {
return each_limit(
$iterable,
$concurrency,
$onFulfilled,
function ($reason, $idx, PromiseInterface $aggregate) {
$aggregate->reject($reason);
}
);
}
}

46
vendor/guzzlehttp/promises/src/Is.php vendored Normal file
View File

@ -0,0 +1,46 @@
<?php
namespace GuzzleHttp\Promise;
final class Is
{
/**
* Returns true if a promise is pending.
*
* @return bool
*/
public static function pending(PromiseInterface $promise)
{
return $promise->getState() === PromiseInterface::PENDING;
}
/**
* Returns true if a promise is fulfilled or rejected.
*
* @return bool
*/
public static function settled(PromiseInterface $promise)
{
return $promise->getState() !== PromiseInterface::PENDING;
}
/**
* Returns true if a promise is fulfilled.
*
* @return bool
*/
public static function fulfilled(PromiseInterface $promise)
{
return $promise->getState() === PromiseInterface::FULFILLED;
}
/**
* Returns true if a promise is rejected.
*
* @return bool
*/
public static function rejected(PromiseInterface $promise)
{
return $promise->getState() === PromiseInterface::REJECTED;
}
}

276
vendor/guzzlehttp/promises/src/Utils.php vendored Normal file
View File

@ -0,0 +1,276 @@
<?php
namespace GuzzleHttp\Promise;
final class Utils
{
/**
* Get the global task queue used for promise resolution.
*
* This task queue MUST be run in an event loop in order for promises to be
* settled asynchronously. It will be automatically run when synchronously
* waiting on a promise.
*
* <code>
* while ($eventLoop->isRunning()) {
* GuzzleHttp\Promise\Utils::queue()->run();
* }
* </code>
*
* @param TaskQueueInterface $assign Optionally specify a new queue instance.
*
* @return TaskQueueInterface
*/
public static function queue(TaskQueueInterface $assign = null)
{
static $queue;
if ($assign) {
$queue = $assign;
} elseif (!$queue) {
$queue = new TaskQueue();
}
return $queue;
}
/**
* Adds a function to run in the task queue when it is next `run()` and
* returns a promise that is fulfilled or rejected with the result.
*
* @param callable $task Task function to run.
*
* @return PromiseInterface
*/
public static function task(callable $task)
{
$queue = self::queue();
$promise = new Promise([$queue, 'run']);
$queue->add(function () use ($task, $promise) {
try {
if (Is::pending($promise)) {
$promise->resolve($task());
}
} catch (\Throwable $e) {
$promise->reject($e);
} catch (\Exception $e) {
$promise->reject($e);
}
});
return $promise;
}
/**
* Synchronously waits on a promise to resolve and returns an inspection
* state array.
*
* Returns a state associative array containing a "state" key mapping to a
* valid promise state. If the state of the promise is "fulfilled", the
* array will contain a "value" key mapping to the fulfilled value of the
* promise. If the promise is rejected, the array will contain a "reason"
* key mapping to the rejection reason of the promise.
*
* @param PromiseInterface $promise Promise or value.
*
* @return array
*/
public static function inspect(PromiseInterface $promise)
{
try {
return [
'state' => PromiseInterface::FULFILLED,
'value' => $promise->wait()
];
} catch (RejectionException $e) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e->getReason()];
} catch (\Throwable $e) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
} catch (\Exception $e) {
return ['state' => PromiseInterface::REJECTED, 'reason' => $e];
}
}
/**
* Waits on all of the provided promises, but does not unwrap rejected
* promises as thrown exception.
*
* Returns an array of inspection state arrays.
*
* @see inspect for the inspection state array format.
*
* @param PromiseInterface[] $promises Traversable of promises to wait upon.
*
* @return array
*/
public static function inspectAll($promises)
{
$results = [];
foreach ($promises as $key => $promise) {
$results[$key] = inspect($promise);
}
return $results;
}
/**
* Waits on all of the provided promises and returns the fulfilled values.
*
* Returns an array that contains the value of each promise (in the same
* order the promises were provided). An exception is thrown if any of the
* promises are rejected.
*
* @param iterable<PromiseInterface> $promises Iterable of PromiseInterface objects to wait on.
*
* @return array
*
* @throws \Exception on error
* @throws \Throwable on error in PHP >=7
*/
public static function unwrap($promises)
{
$results = [];
foreach ($promises as $key => $promise) {
$results[$key] = $promise->wait();
}
return $results;
}
/**
* Given an array of promises, return a promise that is fulfilled when all
* the items in the array are fulfilled.
*
* The promise's fulfillment value is an array with fulfillment values at
* respective positions to the original array. If any promise in the array
* rejects, the returned promise is rejected with the rejection reason.
*
* @param mixed $promises Promises or values.
* @param bool $recursive If true, resolves new promises that might have been added to the stack during its own resolution.
*
* @return PromiseInterface
*/
public static function all($promises, $recursive = false)
{
$results = [];
$promise = Each::of(
$promises,
function ($value, $idx) use (&$results) {
$results[$idx] = $value;
},
function ($reason, $idx, Promise $aggregate) {
$aggregate->reject($reason);
}
)->then(function () use (&$results) {
ksort($results);
return $results;
});
if (true === $recursive) {
$promise = $promise->then(function ($results) use ($recursive, &$promises) {
foreach ($promises as $promise) {
if (Is::pending($promise)) {
return self::all($promises, $recursive);
}
}
return $results;
});
}
return $promise;
}
/**
* Initiate a competitive race between multiple promises or values (values
* will become immediately fulfilled promises).
*
* When count amount of promises have been fulfilled, the returned promise
* is fulfilled with an array that contains the fulfillment values of the
* winners in order of resolution.
*
* This promise is rejected with a {@see AggregateException} if the number
* of fulfilled promises is less than the desired $count.
*
* @param int $count Total number of promises.
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*/
public static function some($count, $promises)
{
$results = [];
$rejections = [];
return Each::of(
$promises,
function ($value, $idx, PromiseInterface $p) use (&$results, $count) {
if (Is::settled($p)) {
return;
}
$results[$idx] = $value;
if (count($results) >= $count) {
$p->resolve(null);
}
},
function ($reason) use (&$rejections) {
$rejections[] = $reason;
}
)->then(
function () use (&$results, &$rejections, $count) {
if (count($results) !== $count) {
throw new AggregateException(
'Not enough promises to fulfill count',
$rejections
);
}
ksort($results);
return array_values($results);
}
);
}
/**
* Like some(), with 1 as count. However, if the promise fulfills, the
* fulfillment value is not an array of 1 but the value directly.
*
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*/
public static function any($promises)
{
return self::some(1, $promises)->then(function ($values) {
return $values[0];
});
}
/**
* Returns a promise that is fulfilled when all of the provided promises have
* been fulfilled or rejected.
*
* The returned promise is fulfilled with an array of inspection state arrays.
*
* @see inspect for the inspection state array format.
*
* @param mixed $promises Promises or values.
*
* @return PromiseInterface
*/
public static function settle($promises)
{
$results = [];
return Each::of(
$promises,
function ($value, $idx) use (&$results) {
$results[$idx] = ['state' => PromiseInterface::FULFILLED, 'value' => $value];
},
function ($reason, $idx) use (&$results) {
$results[$idx] = ['state' => PromiseInterface::REJECTED, 'reason' => $reason];
}
)->then(function () use (&$results) {
ksort($results);
return $results;
});
}
}

View File

@ -0,0 +1,2 @@
github: [Nyholm, GrahamCampbell]
tidelift: "packagist/guzzlehttp/psr7"

View File

@ -0,0 +1,14 @@
daysUntilStale: 120
daysUntilClose: 14
exemptLabels:
- lifecycle/keep-open
- lifecycle/ready-for-merge
# Label to use when marking an issue as stale
staleLabel: lifecycle/stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed after 2 weeks if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View File

@ -0,0 +1,34 @@
name: CI
on:
pull_request:
jobs:
build:
name: Build
runs-on: ubuntu-latest
strategy:
max-parallel: 10
matrix:
php: ['5.5', '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1']
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: 'none'
extensions: mbstring
- name: Checkout code
uses: actions/checkout@v2
- name: Mimic PHP 8.0
run: composer config platform.php 8.0.999
if: matrix.php > 8
- name: Install dependencies
run: composer update --no-interaction --no-progress
- name: Run tests
run: make test

View File

@ -0,0 +1,37 @@
name: Integration
on:
pull_request:
jobs:
build:
name: Test
runs-on: ubuntu-latest
strategy:
max-parallel: 10
matrix:
php: ['7.2', '7.3', '7.4', '8.0']
steps:
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: none
- name: Checkout code
uses: actions/checkout@v2
- name: Download dependencies
uses: ramsey/composer-install@v1
with:
composer-options: --no-interaction --optimize-autoloader
- name: Start server
run: php -S 127.0.0.1:10002 tests/Integration/server.php &
- name: Run tests
env:
TEST_SERVER: 127.0.0.1:10002
run: ./vendor/bin/phpunit --testsuite Integration

View File

@ -0,0 +1,29 @@
name: Static analysis
on:
pull_request:
jobs:
php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
coverage: none
extensions: mbstring
- name: Download dependencies
run: composer update --no-interaction --no-progress
- name: Download PHP CS Fixer
run: composer require "friendsofphp/php-cs-fixer:2.18.4"
- name: Execute PHP CS Fixer
run: vendor/bin/php-cs-fixer fix --diff-format udiff --dry-run

56
vendor/guzzlehttp/psr7/.php_cs.dist vendored Normal file
View File

@ -0,0 +1,56 @@
<?php
$config = PhpCsFixer\Config::create()
->setRiskyAllowed(true)
->setRules([
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'concat_space' => ['spacing' => 'one'],
'declare_strict_types' => false,
'final_static_access' => true,
'fully_qualified_strict_types' => true,
'header_comment' => false,
'is_null' => ['use_yoda_style' => true],
'list_syntax' => ['syntax' => 'long'],
'lowercase_cast' => true,
'magic_method_casing' => true,
'modernize_types_casting' => true,
'multiline_comment_opening_closing' => true,
'no_alias_functions' => true,
'no_alternative_syntax' => true,
'no_blank_lines_after_phpdoc' => true,
'no_empty_comment' => true,
'no_empty_phpdoc' => true,
'no_empty_statement' => true,
'no_extra_blank_lines' => true,
'no_leading_import_slash' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_unset_cast' => true,
'no_unused_imports' => true,
'no_whitespace_in_blank_line' => true,
'ordered_imports' => true,
'php_unit_ordered_covers' => true,
'php_unit_test_annotation' => ['style' => 'prefix'],
'php_unit_test_case_static_method_calls' => ['call_type' => 'self'],
'phpdoc_align' => ['align' => 'vertical'],
'phpdoc_no_useless_inheritdoc' => true,
'phpdoc_scalar' => true,
'phpdoc_separation' => true,
'phpdoc_single_line_var_spacing' => true,
'phpdoc_trim' => true,
'phpdoc_trim_consecutive_blank_line_separation' => true,
'phpdoc_types' => true,
'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'],
'phpdoc_var_without_name' => true,
'single_trait_insert_per_statement' => true,
'standardize_not_equals' => true,
])
->setFinder(
PhpCsFixer\Finder::create()
->in(__DIR__.'/src')
->in(__DIR__.'/tests')
->name('*.php')
)
;
return $config;

71
vendor/guzzlehttp/psr7/src/Header.php vendored Normal file
View File

@ -0,0 +1,71 @@
<?php
namespace GuzzleHttp\Psr7;
final class Header
{
/**
* Parse an array of header values containing ";" separated data into an
* array of associative arrays representing the header key value pair data
* of the header. When a parameter does not contain a value, but just
* contains a key, this function will inject a key with a '' string value.
*
* @param string|array $header Header to parse into components.
*
* @return array Returns the parsed header values.
*/
public static function parse($header)
{
static $trimmed = "\"' \n\t\r";
$params = $matches = [];
foreach (self::normalize($header) as $val) {
$part = [];
foreach (preg_split('/;(?=([^"]*"[^"]*")*[^"]*$)/', $val) as $kvp) {
if (preg_match_all('/<[^>]+>|[^=]+/', $kvp, $matches)) {
$m = $matches[0];
if (isset($m[1])) {
$part[trim($m[0], $trimmed)] = trim($m[1], $trimmed);
} else {
$part[] = trim($m[0], $trimmed);
}
}
}
if ($part) {
$params[] = $part;
}
}
return $params;
}
/**
* Converts an array of header values that may contain comma separated
* headers into an array of headers with no comma separated values.
*
* @param string|array $header Header to normalize.
*
* @return array Returns the normalized header field values.
*/
public static function normalize($header)
{
if (!is_array($header)) {
return array_map('trim', explode(',', $header));
}
$result = [];
foreach ($header as $value) {
foreach ((array) $value as $v) {
if (strpos($v, ',') === false) {
$result[] = $v;
continue;
}
foreach (preg_split('/,(?=([^"]*"[^"]*")*[^"]*$)/', $v) as $vv) {
$result[] = trim($vv);
}
}
}
return $result;
}
}

252
vendor/guzzlehttp/psr7/src/Message.php vendored Normal file
View File

@ -0,0 +1,252 @@
<?php
namespace GuzzleHttp\Psr7;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
final class Message
{
/**
* Returns the string representation of an HTTP message.
*
* @param MessageInterface $message Message to convert to a string.
*
* @return string
*/
public static function toString(MessageInterface $message)
{
if ($message instanceof RequestInterface) {
$msg = trim($message->getMethod() . ' '
. $message->getRequestTarget())
. ' HTTP/' . $message->getProtocolVersion();
if (!$message->hasHeader('host')) {
$msg .= "\r\nHost: " . $message->getUri()->getHost();
}
} elseif ($message instanceof ResponseInterface) {
$msg = 'HTTP/' . $message->getProtocolVersion() . ' '
. $message->getStatusCode() . ' '
. $message->getReasonPhrase();
} else {
throw new \InvalidArgumentException('Unknown message type');
}
foreach ($message->getHeaders() as $name => $values) {
if (strtolower($name) === 'set-cookie') {
foreach ($values as $value) {
$msg .= "\r\n{$name}: " . $value;
}
} else {
$msg .= "\r\n{$name}: " . implode(', ', $values);
}
}
return "{$msg}\r\n\r\n" . $message->getBody();
}
/**
* Get a short summary of the message body.
*
* Will return `null` if the response is not printable.
*
* @param MessageInterface $message The message to get the body summary
* @param int $truncateAt The maximum allowed size of the summary
*
* @return string|null
*/
public static function bodySummary(MessageInterface $message, $truncateAt = 120)
{
$body = $message->getBody();
if (!$body->isSeekable() || !$body->isReadable()) {
return null;
}
$size = $body->getSize();
if ($size === 0) {
return null;
}
$summary = $body->read($truncateAt);
$body->rewind();
if ($size > $truncateAt) {
$summary .= ' (truncated...)';
}
// Matches any printable character, including unicode characters:
// letters, marks, numbers, punctuation, spacing, and separators.
if (preg_match('/[^\pL\pM\pN\pP\pS\pZ\n\r\t]/u', $summary)) {
return null;
}
return $summary;
}
/**
* Attempts to rewind a message body and throws an exception on failure.
*
* The body of the message will only be rewound if a call to `tell()`
* returns a value other than `0`.
*
* @param MessageInterface $message Message to rewind
*
* @throws \RuntimeException
*/
public static function rewindBody(MessageInterface $message)
{
$body = $message->getBody();
if ($body->tell()) {
$body->rewind();
}
}
/**
* Parses an HTTP message into an associative array.
*
* The array contains the "start-line" key containing the start line of
* the message, "headers" key containing an associative array of header
* array values, and a "body" key containing the body of the message.
*
* @param string $message HTTP request or response to parse.
*
* @return array
*/
public static function parseMessage($message)
{
if (!$message) {
throw new \InvalidArgumentException('Invalid message');
}
$message = ltrim($message, "\r\n");
$messageParts = preg_split("/\r?\n\r?\n/", $message, 2);
if ($messageParts === false || count($messageParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing header delimiter');
}
list($rawHeaders, $body) = $messageParts;
$rawHeaders .= "\r\n"; // Put back the delimiter we split previously
$headerParts = preg_split("/\r?\n/", $rawHeaders, 2);
if ($headerParts === false || count($headerParts) !== 2) {
throw new \InvalidArgumentException('Invalid message: Missing status line');
}
list($startLine, $rawHeaders) = $headerParts;
if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') {
// Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0
$rawHeaders = preg_replace(Rfc7230::HEADER_FOLD_REGEX, ' ', $rawHeaders);
}
/** @var array[] $headerLines */
$count = preg_match_all(Rfc7230::HEADER_REGEX, $rawHeaders, $headerLines, PREG_SET_ORDER);
// If these aren't the same, then one line didn't match and there's an invalid header.
if ($count !== substr_count($rawHeaders, "\n")) {
// Folding is deprecated, see https://tools.ietf.org/html/rfc7230#section-3.2.4
if (preg_match(Rfc7230::HEADER_FOLD_REGEX, $rawHeaders)) {
throw new \InvalidArgumentException('Invalid header syntax: Obsolete line folding');
}
throw new \InvalidArgumentException('Invalid header syntax');
}
$headers = [];
foreach ($headerLines as $headerLine) {
$headers[$headerLine[1]][] = $headerLine[2];
}
return [
'start-line' => $startLine,
'headers' => $headers,
'body' => $body,
];
}
/**
* Constructs a URI for an HTTP request message.
*
* @param string $path Path from the start-line
* @param array $headers Array of headers (each value an array).
*
* @return string
*/
public static function parseRequestUri($path, array $headers)
{
$hostKey = array_filter(array_keys($headers), function ($k) {
return strtolower($k) === 'host';
});
// If no host is found, then a full URI cannot be constructed.
if (!$hostKey) {
return $path;
}
$host = $headers[reset($hostKey)][0];
$scheme = substr($host, -4) === ':443' ? 'https' : 'http';
return $scheme . '://' . $host . '/' . ltrim($path, '/');
}
/**
* Parses a request message string into a request object.
*
* @param string $message Request message string.
*
* @return Request
*/
public static function parseRequest($message)
{
$data = self::parseMessage($message);
$matches = [];
if (!preg_match('/^[\S]+\s+([a-zA-Z]+:\/\/|\/).*/', $data['start-line'], $matches)) {
throw new \InvalidArgumentException('Invalid request string');
}
$parts = explode(' ', $data['start-line'], 3);
$version = isset($parts[2]) ? explode('/', $parts[2])[1] : '1.1';
$request = new Request(
$parts[0],
$matches[1] === '/' ? self::parseRequestUri($parts[1], $data['headers']) : $parts[1],
$data['headers'],
$data['body'],
$version
);
return $matches[1] === '/' ? $request : $request->withRequestTarget($parts[1]);
}
/**
* Parses a response message string into a response object.
*
* @param string $message Response message string.
*
* @return Response
*/
public static function parseResponse($message)
{
$data = self::parseMessage($message);
// According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space
// between status-code and reason-phrase is required. But browsers accept
// responses without space and reason as well.
if (!preg_match('/^HTTP\/.* [0-9]{3}( .*|$)/', $data['start-line'])) {
throw new \InvalidArgumentException('Invalid response string: ' . $data['start-line']);
}
$parts = explode(' ', $data['start-line'], 3);
return new Response(
(int) $parts[1],
$data['headers'],
$data['body'],
explode('/', $parts[0])[1],
isset($parts[2]) ? $parts[2] : null
);
}
}

140
vendor/guzzlehttp/psr7/src/MimeType.php vendored Normal file
View File

@ -0,0 +1,140 @@
<?php
namespace GuzzleHttp\Psr7;
final class MimeType
{
/**
* Determines the mimetype of a file by looking at its extension.
*
* @param string $filename
*
* @return string|null
*/
public static function fromFilename($filename)
{
return self::fromExtension(pathinfo($filename, PATHINFO_EXTENSION));
}
/**
* Maps a file extensions to a mimetype.
*
* @param string $extension string The file extension.
*
* @return string|null
*
* @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types
*/
public static function fromExtension($extension)
{
static $mimetypes = [
'3gp' => 'video/3gpp',
'7z' => 'application/x-7z-compressed',
'aac' => 'audio/x-aac',
'ai' => 'application/postscript',
'aif' => 'audio/x-aiff',
'asc' => 'text/plain',
'asf' => 'video/x-ms-asf',
'atom' => 'application/atom+xml',
'avi' => 'video/x-msvideo',
'bmp' => 'image/bmp',
'bz2' => 'application/x-bzip2',
'cer' => 'application/pkix-cert',
'crl' => 'application/pkix-crl',
'crt' => 'application/x-x509-ca-cert',
'css' => 'text/css',
'csv' => 'text/csv',
'cu' => 'application/cu-seeme',
'deb' => 'application/x-debian-package',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'dvi' => 'application/x-dvi',
'eot' => 'application/vnd.ms-fontobject',
'eps' => 'application/postscript',
'epub' => 'application/epub+zip',
'etx' => 'text/x-setext',
'flac' => 'audio/flac',
'flv' => 'video/x-flv',
'gif' => 'image/gif',
'gz' => 'application/gzip',
'htm' => 'text/html',
'html' => 'text/html',
'ico' => 'image/x-icon',
'ics' => 'text/calendar',
'ini' => 'text/plain',
'iso' => 'application/x-iso9660-image',
'jar' => 'application/java-archive',
'jpe' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'js' => 'text/javascript',
'json' => 'application/json',
'latex' => 'application/x-latex',
'log' => 'text/plain',
'm4a' => 'audio/mp4',
'm4v' => 'video/mp4',
'mid' => 'audio/midi',
'midi' => 'audio/midi',
'mov' => 'video/quicktime',
'mkv' => 'video/x-matroska',
'mp3' => 'audio/mpeg',
'mp4' => 'video/mp4',
'mp4a' => 'audio/mp4',
'mp4v' => 'video/mp4',
'mpe' => 'video/mpeg',
'mpeg' => 'video/mpeg',
'mpg' => 'video/mpeg',
'mpg4' => 'video/mp4',
'oga' => 'audio/ogg',
'ogg' => 'audio/ogg',
'ogv' => 'video/ogg',
'ogx' => 'application/ogg',
'pbm' => 'image/x-portable-bitmap',
'pdf' => 'application/pdf',
'pgm' => 'image/x-portable-graymap',
'png' => 'image/png',
'pnm' => 'image/x-portable-anymap',
'ppm' => 'image/x-portable-pixmap',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'ps' => 'application/postscript',
'qt' => 'video/quicktime',
'rar' => 'application/x-rar-compressed',
'ras' => 'image/x-cmu-raster',
'rss' => 'application/rss+xml',
'rtf' => 'application/rtf',
'sgm' => 'text/sgml',
'sgml' => 'text/sgml',
'svg' => 'image/svg+xml',
'swf' => 'application/x-shockwave-flash',
'tar' => 'application/x-tar',
'tif' => 'image/tiff',
'tiff' => 'image/tiff',
'torrent' => 'application/x-bittorrent',
'ttf' => 'application/x-font-ttf',
'txt' => 'text/plain',
'wav' => 'audio/x-wav',
'webm' => 'video/webm',
'webp' => 'image/webp',
'wma' => 'audio/x-ms-wma',
'wmv' => 'video/x-ms-wmv',
'woff' => 'application/x-font-woff',
'wsdl' => 'application/wsdl+xml',
'xbm' => 'image/x-xbitmap',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'xml' => 'application/xml',
'xpm' => 'image/x-xpixmap',
'xwd' => 'image/x-xwindowdump',
'yaml' => 'text/yaml',
'yml' => 'text/yaml',
'zip' => 'application/zip',
];
$extension = strtolower($extension);
return isset($mimetypes[$extension])
? $mimetypes[$extension]
: null;
}
}

113
vendor/guzzlehttp/psr7/src/Query.php vendored Normal file
View File

@ -0,0 +1,113 @@
<?php
namespace GuzzleHttp\Psr7;
final class Query
{
/**
* Parse a query string into an associative array.
*
* If multiple values are found for the same key, the value of that key
* value pair will become an array. This function does not parse nested
* PHP style arrays into an associative array (e.g., `foo[a]=1&foo[b]=2`
* will be parsed into `['foo[a]' => '1', 'foo[b]' => '2'])`.
*
* @param string $str Query string to parse
* @param int|bool $urlEncoding How the query string is encoded
*
* @return array
*/
public static function parse($str, $urlEncoding = true)
{
$result = [];
if ($str === '') {
return $result;
}
if ($urlEncoding === true) {
$decoder = function ($value) {
return rawurldecode(str_replace('+', ' ', $value));
};
} elseif ($urlEncoding === PHP_QUERY_RFC3986) {
$decoder = 'rawurldecode';
} elseif ($urlEncoding === PHP_QUERY_RFC1738) {
$decoder = 'urldecode';
} else {
$decoder = function ($str) {
return $str;
};
}
foreach (explode('&', $str) as $kvp) {
$parts = explode('=', $kvp, 2);
$key = $decoder($parts[0]);
$value = isset($parts[1]) ? $decoder($parts[1]) : null;
if (!isset($result[$key])) {
$result[$key] = $value;
} else {
if (!is_array($result[$key])) {
$result[$key] = [$result[$key]];
}
$result[$key][] = $value;
}
}
return $result;
}
/**
* Build a query string from an array of key value pairs.
*
* This function can use the return value of `parse()` to build a query
* string. This function does not modify the provided keys when an array is
* encountered (like `http_build_query()` would).
*
* @param array $params Query string parameters.
* @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986
* to encode using RFC3986, or PHP_QUERY_RFC1738
* to encode using RFC1738.
*
* @return string
*/
public static function build(array $params, $encoding = PHP_QUERY_RFC3986)
{
if (!$params) {
return '';
}
if ($encoding === false) {
$encoder = function ($str) {
return $str;
};
} elseif ($encoding === PHP_QUERY_RFC3986) {
$encoder = 'rawurlencode';
} elseif ($encoding === PHP_QUERY_RFC1738) {
$encoder = 'urlencode';
} else {
throw new \InvalidArgumentException('Invalid type');
}
$qs = '';
foreach ($params as $k => $v) {
$k = $encoder($k);
if (!is_array($v)) {
$qs .= $k;
if ($v !== null) {
$qs .= '=' . $encoder($v);
}
$qs .= '&';
} else {
foreach ($v as $vv) {
$qs .= $k;
if ($vv !== null) {
$qs .= '=' . $encoder($vv);
}
$qs .= '&';
}
}
}
return $qs ? (string) substr($qs, 0, -1) : '';
}
}

19
vendor/guzzlehttp/psr7/src/Rfc7230.php vendored Normal file
View File

@ -0,0 +1,19 @@
<?php
namespace GuzzleHttp\Psr7;
final class Rfc7230
{
/**
* Header related regular expressions (copied from amphp/http package)
* (Note: once we require PHP 7.x we could just depend on the upstream package)
*
* Note: header delimiter (\r\n) is modified to \r?\n to accept line feed only delimiters for BC reasons.
*
* @link https://github.com/amphp/http/blob/v1.0.1/src/Rfc7230.php#L12-L15
*
* @license https://github.com/amphp/http/blob/v1.0.1/LICENSE
*/
const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m";
const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)";
}

View File

@ -0,0 +1,55 @@
<?php
namespace GuzzleHttp\Psr7;
use Psr\Http\Message\UriInterface;
/**
* Provides methods to determine if a modified URL should be considered cross-origin.
*
* @author Graham Campbell
*/
final class UriComparator
{
/**
* Determines if a modified URL should be considered cross-origin with
* respect to an original URL.
*
* @return bool
*/
public static function isCrossOrigin(UriInterface $original, UriInterface $modified)
{
if (\strcasecmp($original->getHost(), $modified->getHost()) !== 0) {
return true;
}
if ($original->getScheme() !== $modified->getScheme()) {
return true;
}
if (self::computePort($original) !== self::computePort($modified)) {
return true;
}
return false;
}
/**
* @return int
*/
private static function computePort(UriInterface $uri)
{
$port = $uri->getPort();
if (null !== $port) {
return $port;
}
return 'https' === $uri->getScheme() ? 443 : 80;
}
private function __construct()
{
// cannot be instantiated
}
}

428
vendor/guzzlehttp/psr7/src/Utils.php vendored Normal file
View File

@ -0,0 +1,428 @@
<?php
namespace GuzzleHttp\Psr7;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Message\UriInterface;
final class Utils
{
/**
* Remove the items given by the keys, case insensitively from the data.
*
* @param iterable<string> $keys
*
* @return array
*/
public static function caselessRemove($keys, array $data)
{
$result = [];
foreach ($keys as &$key) {
$key = strtolower($key);
}
foreach ($data as $k => $v) {
if (!in_array(strtolower($k), $keys)) {
$result[$k] = $v;
}
}
return $result;
}
/**
* Copy the contents of a stream into another stream until the given number
* of bytes have been read.
*
* @param StreamInterface $source Stream to read from
* @param StreamInterface $dest Stream to write to
* @param int $maxLen Maximum number of bytes to read. Pass -1
* to read the entire stream.
*
* @throws \RuntimeException on error.
*/
public static function copyToStream(StreamInterface $source, StreamInterface $dest, $maxLen = -1)
{
$bufferSize = 8192;
if ($maxLen === -1) {
while (!$source->eof()) {
if (!$dest->write($source->read($bufferSize))) {
break;
}
}
} else {
$remaining = $maxLen;
while ($remaining > 0 && !$source->eof()) {
$buf = $source->read(min($bufferSize, $remaining));
$len = strlen($buf);
if (!$len) {
break;
}
$remaining -= $len;
$dest->write($buf);
}
}
}
/**
* Copy the contents of a stream into a string until the given number of
* bytes have been read.
*
* @param StreamInterface $stream Stream to read
* @param int $maxLen Maximum number of bytes to read. Pass -1
* to read the entire stream.
*
* @return string
*
* @throws \RuntimeException on error.
*/
public static function copyToString(StreamInterface $stream, $maxLen = -1)
{
$buffer = '';
if ($maxLen === -1) {
while (!$stream->eof()) {
$buf = $stream->read(1048576);
// Using a loose equality here to match on '' and false.
if ($buf == null) {
break;
}
$buffer .= $buf;
}
return $buffer;
}
$len = 0;
while (!$stream->eof() && $len < $maxLen) {
$buf = $stream->read($maxLen - $len);
// Using a loose equality here to match on '' and false.
if ($buf == null) {
break;
}
$buffer .= $buf;
$len = strlen($buffer);
}
return $buffer;
}
/**
* Calculate a hash of a stream.
*
* This method reads the entire stream to calculate a rolling hash, based
* on PHP's `hash_init` functions.
*
* @param StreamInterface $stream Stream to calculate the hash for
* @param string $algo Hash algorithm (e.g. md5, crc32, etc)
* @param bool $rawOutput Whether or not to use raw output
*
* @return string Returns the hash of the stream
*
* @throws \RuntimeException on error.
*/
public static function hash(StreamInterface $stream, $algo, $rawOutput = false)
{
$pos = $stream->tell();
if ($pos > 0) {
$stream->rewind();
}
$ctx = hash_init($algo);
while (!$stream->eof()) {
hash_update($ctx, $stream->read(1048576));
}
$out = hash_final($ctx, (bool) $rawOutput);
$stream->seek($pos);
return $out;
}
/**
* Clone and modify a request with the given changes.
*
* This method is useful for reducing the number of clones needed to mutate
* a message.
*
* The changes can be one of:
* - method: (string) Changes the HTTP method.
* - set_headers: (array) Sets the given headers.
* - remove_headers: (array) Remove the given headers.
* - body: (mixed) Sets the given body.
* - uri: (UriInterface) Set the URI.
* - query: (string) Set the query string value of the URI.
* - version: (string) Set the protocol version.
*
* @param RequestInterface $request Request to clone and modify.
* @param array $changes Changes to apply.
*
* @return RequestInterface
*/
public static function modifyRequest(RequestInterface $request, array $changes)
{
if (!$changes) {
return $request;
}
$headers = $request->getHeaders();
if (!isset($changes['uri'])) {
$uri = $request->getUri();
} else {
// Remove the host header if one is on the URI
if ($host = $changes['uri']->getHost()) {
$changes['set_headers']['Host'] = $host;
if ($port = $changes['uri']->getPort()) {
$standardPorts = ['http' => 80, 'https' => 443];
$scheme = $changes['uri']->getScheme();
if (isset($standardPorts[$scheme]) && $port != $standardPorts[$scheme]) {
$changes['set_headers']['Host'] .= ':' . $port;
}
}
}
$uri = $changes['uri'];
}
if (!empty($changes['remove_headers'])) {
$headers = self::caselessRemove($changes['remove_headers'], $headers);
}
if (!empty($changes['set_headers'])) {
$headers = self::caselessRemove(array_keys($changes['set_headers']), $headers);
$headers = $changes['set_headers'] + $headers;
}
if (isset($changes['query'])) {
$uri = $uri->withQuery($changes['query']);
}
if ($request instanceof ServerRequestInterface) {
$new = (new ServerRequest(
isset($changes['method']) ? $changes['method'] : $request->getMethod(),
$uri,
$headers,
isset($changes['body']) ? $changes['body'] : $request->getBody(),
isset($changes['version'])
? $changes['version']
: $request->getProtocolVersion(),
$request->getServerParams()
))
->withParsedBody($request->getParsedBody())
->withQueryParams($request->getQueryParams())
->withCookieParams($request->getCookieParams())
->withUploadedFiles($request->getUploadedFiles());
foreach ($request->getAttributes() as $key => $value) {
$new = $new->withAttribute($key, $value);
}
return $new;
}
return new Request(
isset($changes['method']) ? $changes['method'] : $request->getMethod(),
$uri,
$headers,
isset($changes['body']) ? $changes['body'] : $request->getBody(),
isset($changes['version'])
? $changes['version']
: $request->getProtocolVersion()
);
}
/**
* Read a line from the stream up to the maximum allowed buffer length.
*
* @param StreamInterface $stream Stream to read from
* @param int|null $maxLength Maximum buffer length
*
* @return string
*/
public static function readLine(StreamInterface $stream, $maxLength = null)
{
$buffer = '';
$size = 0;
while (!$stream->eof()) {
// Using a loose equality here to match on '' and false.
if (null == ($byte = $stream->read(1))) {
return $buffer;
}
$buffer .= $byte;
// Break when a new line is found or the max length - 1 is reached
if ($byte === "\n" || ++$size === $maxLength - 1) {
break;
}
}
return $buffer;
}
/**
* Create a new stream based on the input type.
*
* Options is an associative array that can contain the following keys:
* - metadata: Array of custom metadata.
* - size: Size of the stream.
*
* This method accepts the following `$resource` types:
* - `Psr\Http\Message\StreamInterface`: Returns the value as-is.
* - `string`: Creates a stream object that uses the given string as the contents.
* - `resource`: Creates a stream object that wraps the given PHP stream resource.
* - `Iterator`: If the provided value implements `Iterator`, then a read-only
* stream object will be created that wraps the given iterable. Each time the
* stream is read from, data from the iterator will fill a buffer and will be
* continuously called until the buffer is equal to the requested read size.
* Subsequent read calls will first read from the buffer and then call `next`
* on the underlying iterator until it is exhausted.
* - `object` with `__toString()`: If the object has the `__toString()` method,
* the object will be cast to a string and then a stream will be returned that
* uses the string value.
* - `NULL`: When `null` is passed, an empty stream object is returned.
* - `callable` When a callable is passed, a read-only stream object will be
* created that invokes the given callable. The callable is invoked with the
* number of suggested bytes to read. The callable can return any number of
* bytes, but MUST return `false` when there is no more data to return. The
* stream object that wraps the callable will invoke the callable until the
* number of requested bytes are available. Any additional bytes will be
* buffered and used in subsequent reads.
*
* @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data
* @param array $options Additional options
*
* @return StreamInterface
*
* @throws \InvalidArgumentException if the $resource arg is not valid.
*/
public static function streamFor($resource = '', array $options = [])
{
if (is_scalar($resource)) {
$stream = self::tryFopen('php://temp', 'r+');
if ($resource !== '') {
fwrite($stream, $resource);
fseek($stream, 0);
}
return new Stream($stream, $options);
}
switch (gettype($resource)) {
case 'resource':
/*
* The 'php://input' is a special stream with quirks and inconsistencies.
* We avoid using that stream by reading it into php://temp
*/
$metaData = \stream_get_meta_data($resource);
if (isset($metaData['uri']) && $metaData['uri'] === 'php://input') {
$stream = self::tryFopen('php://temp', 'w+');
fwrite($stream, stream_get_contents($resource));
fseek($stream, 0);
$resource = $stream;
}
return new Stream($resource, $options);
case 'object':
if ($resource instanceof StreamInterface) {
return $resource;
} elseif ($resource instanceof \Iterator) {
return new PumpStream(function () use ($resource) {
if (!$resource->valid()) {
return false;
}
$result = $resource->current();
$resource->next();
return $result;
}, $options);
} elseif (method_exists($resource, '__toString')) {
return Utils::streamFor((string) $resource, $options);
}
break;
case 'NULL':
return new Stream(self::tryFopen('php://temp', 'r+'), $options);
}
if (is_callable($resource)) {
return new PumpStream($resource, $options);
}
throw new \InvalidArgumentException('Invalid resource type: ' . gettype($resource));
}
/**
* Safely opens a PHP stream resource using a filename.
*
* When fopen fails, PHP normally raises a warning. This function adds an
* error handler that checks for errors and throws an exception instead.
*
* @param string $filename File to open
* @param string $mode Mode used to open the file
*
* @return resource
*
* @throws \RuntimeException if the file cannot be opened
*/
public static function tryFopen($filename, $mode)
{
$ex = null;
set_error_handler(function () use ($filename, $mode, &$ex) {
$ex = new \RuntimeException(sprintf(
'Unable to open "%s" using mode "%s": %s',
$filename,
$mode,
func_get_args()[1]
));
return true;
});
try {
$handle = fopen($filename, $mode);
} catch (\Throwable $e) {
$ex = new \RuntimeException(sprintf(
'Unable to open "%s" using mode "%s": %s',
$filename,
$mode,
$e->getMessage()
), 0, $e);
}
restore_error_handler();
if ($ex) {
/** @var $ex \RuntimeException */
throw $ex;
}
return $handle;
}
/**
* Returns a UriInterface for the given value.
*
* This function accepts a string or UriInterface and returns a
* UriInterface for the given value. If the value is already a
* UriInterface, it is returned as-is.
*
* @param string|UriInterface $uri
*
* @return UriInterface
*
* @throws \InvalidArgumentException
*/
public static function uriFor($uri)
{
if ($uri instanceof UriInterface) {
return $uri;
}
if (is_string($uri)) {
return new Uri($uri);
}
throw new \InvalidArgumentException('URI must be a string or UriInterface');
}
}

12
vendor/guzzlehttp/ringphp/.editorconfig vendored Normal file
View File

@ -0,0 +1,12 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[{Makefile,*.mk}]
indent_style = tab

4
vendor/guzzlehttp/ringphp/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
vendor
build/artifacts/
composer.lock
docs/_build/

43
vendor/guzzlehttp/ringphp/.travis.yml vendored Normal file
View File

@ -0,0 +1,43 @@
language: php
cache:
directories:
- $HOME/.composer/cache/files
php:
- 5.4
- 5.5
- 5.6
- 7.0
- 7.1
- 7.2
- hhvm
- nightly
env:
global:
- TEST_COMMAND="composer test"
matrix:
allow_failures:
- php: hhvm
- php: nightly
fast_finish: true
include:
- php: 5.4
env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest"
before_install:
- if [[ $COVERAGE != true ]]; then phpenv config-rm xdebug.ini || true; fi
install:
# To be removed when this issue will be resolved: https://github.com/composer/composer/issues/5355
- if [[ "$COMPOSER_FLAGS" == *"--prefer-lowest"* ]]; then travis_retry composer update --prefer-dist --no-interaction --prefer-stable --quiet; fi
- travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction
before_script:
- ~/.nvm/nvm.sh install v0.6.14
- ~/.nvm/nvm.sh run v0.6.14
script:
- $TEST_COMMAND

118
vendor/guzzlehttp/ringphp/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,118 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [1.1.1] - 2018-07-31
### Fixed
- `continue` keyword usage on PHP 7.3
## [1.1.0] - 2015-05-19
### Added
- Added `CURL_HTTP_VERSION_2_0`
### Changed
- The PHP stream wrapper handler now sets `allow_self_signed` to `false` to
match the cURL handler when `verify` is set to `true` or a certificate file.
- Ensuring that a directory exists before using the `save_to` option.
- Response protocol version is now correctly extracted from a response.
### Fixed
- Fixed a bug in which the result of `CurlFactory::retryFailedRewind` did not
return an array.
## [1.0.7] - 2015-03-29
### Fixed
- PHP 7 fixes.
## [1.0.6] - 2015-02-26
### Changed
- The multi handle of the CurlMultiHandler is now created lazily.
### Fixed
- Bug fix: futures now extend from React's PromiseInterface to ensure that they
are properly forwarded down the promise chain.
## [1.0.5] - 2014-12-10
### Added
- Adding more error information to PHP stream wrapper exceptions.
- Added digest auth integration test support to test server.
## [1.0.4] - 2014-12-01
### Added
- Added support for older versions of cURL that do not have CURLOPT_TIMEOUT_MS.
- Added a fix to the StreamHandler to return a `FutureArrayInterface` when an
### Changed
- Setting debug to `false` does not enable debug output. error occurs.
## [1.0.3] - 2014-11-03
### Fixed
- Setting the `header` stream option as a string to be compatible with GAE.
- Header parsing now ensures that header order is maintained in the parsed
message.
## [1.0.2] - 2014-10-28
### Fixed
- Now correctly honoring a `version` option is supplied in a request.
See https://github.com/guzzle/RingPHP/pull/8
## [1.0.1] - 2014-10-26
### Fixed
- Fixed a header parsing issue with the `CurlHandler` and `CurlMultiHandler`
that caused cURL requests with multiple responses to merge repsonses together
(e.g., requests with digest authentication).
## 1.0.0 - 2014-10-12
- Initial release
[Unreleased]: https://github.com/guzzle/RingPHP/compare/1.1.1...HEAD
[1.1.1]: https://github.com/guzzle/RingPHP/compare/1.1.0...1.1.1
[1.1.0]: https://github.com/guzzle/RingPHP/compare/1.0.7...1.1.0
[1.0.7]: https://github.com/guzzle/RingPHP/compare/1.0.6...1.0.7
[1.0.6]: https://github.com/guzzle/RingPHP/compare/1.0.5...1.0.6
[1.0.5]: https://github.com/guzzle/RingPHP/compare/1.0.4...1.0.5
[1.0.4]: https://github.com/guzzle/RingPHP/compare/1.0.3...1.0.4
[1.0.3]: https://github.com/guzzle/RingPHP/compare/1.0.2...1.0.3
[1.0.2]: https://github.com/guzzle/RingPHP/compare/1.0.1...1.0.2
[1.0.1]: https://github.com/guzzle/RingPHP/compare/1.0.0...1.0.1

19
vendor/guzzlehttp/ringphp/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2014 Michael Dowling, https://github.com/mtdowling <mtdowling@gmail.com>
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.

46
vendor/guzzlehttp/ringphp/Makefile vendored Normal file
View File

@ -0,0 +1,46 @@
all: clean coverage docs
docs:
cd docs && make html
view-docs:
open docs/_build/html/index.html
start-server: stop-server
node tests/Client/server.js &> /dev/null &
stop-server:
@PID=$(shell ps axo pid,command \
| grep 'tests/Client/server.js' \
| grep -v grep \
| cut -f 1 -d " "\
) && [ -n "$$PID" ] && kill $$PID || true
test: start-server
vendor/bin/phpunit $(TEST)
$(MAKE) stop-server
coverage: start-server
vendor/bin/phpunit --coverage-html=build/artifacts/coverage $(TEST)
$(MAKE) stop-server
view-coverage:
open build/artifacts/coverage/index.html
clean:
rm -rf build/artifacts/*
cd docs && make clean
tag:
$(if $(TAG),,$(error TAG is not defined. Pass via "make tag TAG=4.2.1"))
@echo Tagging $(TAG)
chag update -m '$(TAG) ()'
git add -A
git commit -m '$(TAG) release'
chag tag
perf: start-server
php tests/perf.php
$(MAKE) stop-server
.PHONY: docs

46
vendor/guzzlehttp/ringphp/README.rst vendored Normal file
View File

@ -0,0 +1,46 @@
=======
RingPHP
=======
Provides a simple API and specification that abstracts away the details of HTTP
into a single PHP function. RingPHP be used to power HTTP clients and servers
through a PHP function that accepts a request hash and returns a response hash
that is fulfilled using a `promise <https://github.com/reactphp/promise>`_,
allowing RingPHP to support both synchronous and asynchronous workflows.
By abstracting the implementation details of different HTTP clients and
servers, RingPHP allows you to utilize pluggable HTTP clients and servers
without tying your application to a specific implementation.
.. code-block:: php
<?php
require 'vendor/autoload.php';
use GuzzleHttp\Ring\Client\CurlHandler;
$handler = new CurlHandler();
$response = $handler([
'http_method' => 'GET',
'uri' => '/',
'headers' => [
'host' => ['www.google.com'],
'x-foo' => ['baz']
]
]);
$response->then(function (array $response) {
echo $response['status'];
});
$response->wait();
RingPHP is inspired by Clojure's `Ring <https://github.com/ring-clojure/ring>`_,
which, in turn, was inspired by Python's WSGI and Ruby's Rack. RingPHP is
utilized as the handler layer in `Guzzle <http://guzzlephp.org>`_ 5.0+ to send
HTTP requests.
Documentation
-------------
See http://ringphp.readthedocs.org/ for the full online documentation.

43
vendor/guzzlehttp/ringphp/composer.json vendored Normal file
View File

@ -0,0 +1,43 @@
{
"name": "guzzlehttp/ringphp",
"description": "Provides a simple API and specification that abstracts away the details of HTTP into a single PHP function.",
"license": "MIT",
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"require": {
"php": ">=5.4.0",
"guzzlehttp/streams": "~3.0",
"react/promise": "~2.0"
},
"require-dev": {
"ext-curl": "*",
"phpunit/phpunit": "~4.0"
},
"suggest": {
"ext-curl": "Guzzle will use specific adapters if cURL is present"
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Ring\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"GuzzleHttp\\Tests\\Ring\\": "tests/"
}
},
"scripts": {
"test": "make test",
"test-ci": "make coverage"
},
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
}
}

153
vendor/guzzlehttp/ringphp/docs/Makefile vendored Normal file
View File

@ -0,0 +1,153 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/GuzzleRing.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/GuzzleRing.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/GuzzleRing"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/GuzzleRing"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."

View File

@ -0,0 +1,173 @@
===============
Client Handlers
===============
Client handlers accept a request array and return a future response array that
can be used synchronously as an array or asynchronously using a promise.
Built-In Handlers
-----------------
RingPHP comes with three built-in client handlers.
Stream Handler
~~~~~~~~~~~~~~
The ``GuzzleHttp\Ring\Client\StreamHandler`` uses PHP's
`http stream wrapper <http://php.net/manual/en/wrappers.http.php>`_ to send
requests.
.. note::
This handler cannot send requests concurrently.
You can provide an associative array of custom stream context options to the
StreamHandler using the ``stream_context`` key of the ``client`` request
option.
.. code-block:: php
use GuzzleHttp\Ring\Client\StreamHandler;
$response = $handler([
'http_method' => 'GET',
'uri' => '/',
'headers' => ['host' => ['httpbin.org']],
'client' => [
'stream_context' => [
'http' => [
'request_fulluri' => true,
'method' => 'HEAD'
],
'socket' => [
'bindto' => '127.0.0.1:0'
],
'ssl' => [
'verify_peer' => false
]
]
]
]);
// Even though it's already completed, you can still use a promise
$response->then(function ($response) {
echo $response['status']; // 200
});
// Or access the response using the future interface
echo $response['status']; // 200
cURL Handler
~~~~~~~~~~~~
The ``GuzzleHttp\Ring\Client\CurlHandler`` can be used with PHP 5.5+ to send
requests using cURL easy handles. This handler is great for sending requests
one at a time because the execute and select loop is implemented in C code
which executes faster and consumes less memory than using PHP's
``curl_multi_*`` interface.
.. note::
This handler cannot send requests concurrently.
When using the CurlHandler, custom curl options can be specified as an
associative array of `cURL option constants <http://php.net/manual/en/curl.constants.php>`_
mapping to values in the ``client`` option of a requst using the **curl** key.
.. code-block:: php
use GuzzleHttp\Ring\Client\CurlHandler;
$handler = new CurlHandler();
$request = [
'http_method' => 'GET',
'headers' => ['host' => [Server::$host]],
'client' => ['curl' => [CURLOPT_LOW_SPEED_LIMIT => 10]]
];
$response = $handler($request);
// The response can be used directly as an array.
echo $response['status']; // 200
// Or, it can be used as a promise (that has already fulfilled).
$response->then(function ($response) {
echo $response['status']; // 200
});
cURL Multi Handler
~~~~~~~~~~~~~~~~~~
The ``GuzzleHttp\Ring\Client\CurlMultiHandler`` transfers requests using
cURL's `multi API <http://curl.haxx.se/libcurl/c/libcurl-multi.html>`_. The
``CurlMultiHandler`` is great for sending requests concurrently.
.. code-block:: php
use GuzzleHttp\Ring\Client\CurlMultiHandler;
$handler = new CurlMultiHandler();
$request = [
'http_method' => 'GET',
'headers' => ['host' => [Server::$host]]
];
// this call returns a future array immediately.
$response = $handler($request);
// Ideally, you should use the promise API to not block.
$response
->then(function ($response) {
// Got the response at some point in the future
echo $response['status']; // 200
// Don't break the chain
return $response;
})->then(function ($response) {
// ...
});
// If you really need to block, then you can use the response as an
// associative array. This will block until it has completed.
echo $response['status']; // 200
Just like the ``CurlHandler``, the ``CurlMultiHandler`` accepts custom curl
option in the ``curl`` key of the ``client`` request option.
Mock Handler
~~~~~~~~~~~~
The ``GuzzleHttp\Ring\Client\MockHandler`` is used to return mock responses.
When constructed, the handler can be configured to return the same response
array over and over, a future response, or a the evaluation of a callback
function.
.. code-block:: php
use GuzzleHttp\Ring\Client\MockHandler;
// Return a canned response.
$mock = new MockHandler(['status' => 200]);
$response = $mock([]);
assert(200 == $response['status']);
assert([] == $response['headers']);
Implementing Handlers
---------------------
Client handlers are just PHP callables (functions or classes that have the
``__invoke`` magic method). The callable accepts a request array and MUST
return an instance of ``GuzzleHttp\Ring\Future\FutureArrayInterface`` so that
the response can be used by both blocking and non-blocking consumers.
Handlers need to follow a few simple rules:
1. Do not throw exceptions. If an error is encountered, return an array that
contains the ``error`` key that maps to an ``\Exception`` value.
2. If the request has a ``delay`` client option, then the handler should only
send the request after the specified delay time in seconds. Blocking
handlers may find it convenient to just let the
``GuzzleHttp\Ring\Core::doSleep($request)`` function handle this for them.
3. Always return an instance of ``GuzzleHttp\Ring\Future\FutureArrayInterface``.
4. Complete any outstanding requests when the handler is destructed.

View File

@ -0,0 +1,165 @@
=================
Client Middleware
=================
Middleware intercepts requests before they are sent over the wire and can be
used to add functionality to handlers.
Modifying Requests
------------------
Let's say you wanted to modify requests before they are sent over the wire
so that they always add specific headers. This can be accomplished by creating
a function that accepts a handler and returns a new function that adds the
composed behavior.
.. code-block:: php
use GuzzleHttp\Ring\Client\CurlHandler;
$handler = new CurlHandler();
$addHeaderHandler = function (callable $handler, array $headers = []) {
return function (array $request) use ($handler, $headers) {
// Add our custom headers
foreach ($headers as $key => $value) {
$request['headers'][$key] = $value;
}
// Send the request using the handler and return the response.
return $handler($request);
}
};
// Create a new handler that adds headers to each request.
$handler = $addHeaderHandler($handler, [
'X-AddMe' => 'hello',
'Authorization' => 'Basic xyz'
]);
$response = $handler([
'http_method' => 'GET',
'headers' => ['Host' => ['httpbin.org']]
]);
Modifying Responses
-------------------
You can change a response as it's returned from a middleware. Remember that
responses returned from an handler (including middleware) must implement
``GuzzleHttp\Ring\Future\FutureArrayInterface``. In order to be a good citizen,
you should not expect that the responses returned through your middleware will
be completed synchronously. Instead, you should use the
``GuzzleHttp\Ring\Core::proxy()`` function to modify the response when the
underlying promise is resolved. This function is a helper function that makes it
easy to create a new instance of ``FutureArrayInterface`` that wraps an existing
``FutureArrayInterface`` object.
Let's say you wanted to add headers to a response as they are returned from
your middleware, but you want to make sure you aren't causing future
responses to be dereferenced right away. You can achieve this by modifying the
incoming request and using the ``Core::proxy`` function.
.. code-block:: php
use GuzzleHttp\Ring\Core;
use GuzzleHttp\Ring\Client\CurlHandler;
$handler = new CurlHandler();
$responseHeaderHandler = function (callable $handler, array $headers) {
return function (array $request) use ($handler, $headers) {
// Send the request using the wrapped handler.
return Core::proxy($handler($request), function ($response) use ($headers) {
// Add the headers to the response when it is available.
foreach ($headers as $key => $value) {
$response['headers'][$key] = (array) $value;
}
// Note that you can return a regular response array when using
// the proxy method.
return $response;
});
}
};
// Create a new handler that adds headers to each response.
$handler = $responseHeaderHandler($handler, ['X-Header' => 'hello!']);
$response = $handler([
'http_method' => 'GET',
'headers' => ['Host' => ['httpbin.org']]
]);
assert($response['headers']['X-Header'] == 'hello!');
Built-In Middleware
-------------------
RingPHP comes with a few basic client middlewares that modify requests
and responses.
Streaming Middleware
~~~~~~~~~~~~~~~~~~~~
If you want to send all requests with the ``streaming`` option to a specific
handler but other requests to a different handler, then use the streaming
middleware.
.. code-block:: php
use GuzzleHttp\Ring\Client\CurlHandler;
use GuzzleHttp\Ring\Client\StreamHandler;
use GuzzleHttp\Ring\Client\Middleware;
$defaultHandler = new CurlHandler();
$streamingHandler = new StreamHandler();
$streamingHandler = Middleware::wrapStreaming(
$defaultHandler,
$streamingHandler
);
// Send the request using the streaming handler.
$response = $streamingHandler([
'http_method' => 'GET',
'headers' => ['Host' => ['www.google.com']],
'stream' => true
]);
// Send the request using the default handler.
$response = $streamingHandler([
'http_method' => 'GET',
'headers' => ['Host' => ['www.google.com']]
]);
Future Middleware
~~~~~~~~~~~~~~~~~
If you want to send all requests with the ``future`` option to a specific
handler but other requests to a different handler, then use the future
middleware.
.. code-block:: php
use GuzzleHttp\Ring\Client\CurlHandler;
use GuzzleHttp\Ring\Client\CurlMultiHandler;
use GuzzleHttp\Ring\Client\Middleware;
$defaultHandler = new CurlHandler();
$futureHandler = new CurlMultiHandler();
$futureHandler = Middleware::wrapFuture(
$defaultHandler,
$futureHandler
);
// Send the request using the blocking CurlHandler.
$response = $futureHandler([
'http_method' => 'GET',
'headers' => ['Host' => ['www.google.com']]
]);
// Send the request using the non-blocking CurlMultiHandler.
$response = $futureHandler([
'http_method' => 'GET',
'headers' => ['Host' => ['www.google.com']],
'future' => true
]);

23
vendor/guzzlehttp/ringphp/docs/conf.py vendored Normal file
View File

@ -0,0 +1,23 @@
import sys, os
import sphinx_rtd_theme
from sphinx.highlighting import lexers
from pygments.lexers.web import PhpLexer
lexers['php'] = PhpLexer(startinline=True, linenos=1)
lexers['php-annotations'] = PhpLexer(startinline=True, linenos=1)
primary_domain = 'php'
extensions = []
templates_path = ['_templates']
source_suffix = '.rst'
master_doc = 'index'
project = u'RingPHP'
copyright = u'2014, Michael Dowling'
version = '1.0.0-alpha'
exclude_patterns = ['_build']
html_title = "RingPHP"
html_short_title = "RingPHP"
html_theme = "sphinx_rtd_theme"
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]

Some files were not shown because too many files have changed in this diff Show More