功能合并:插件更新
This commit is contained in:
parent
ff1fa5d79b
commit
406c03a8a1
|
|
@ -0,0 +1,2 @@
|
||||||
|
/.github/workflows/ export-ignore
|
||||||
|
/tests/ export-ignore
|
||||||
|
|
@ -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();
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### 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.
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\Encoding;
|
||||||
|
|
||||||
|
interface EncodingInterface
|
||||||
|
{
|
||||||
|
public function __toString(): string;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\ErrorCorrectionLevel;
|
||||||
|
|
||||||
|
final class ErrorCorrectionLevelHigh implements ErrorCorrectionLevelInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
9
vendor/endroid/qr-code/src/ErrorCorrectionLevel/ErrorCorrectionLevelInterface.php
vendored
Normal file
9
vendor/endroid/qr-code/src/ErrorCorrectionLevel/ErrorCorrectionLevelInterface.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\ErrorCorrectionLevel;
|
||||||
|
|
||||||
|
interface ErrorCorrectionLevelInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\ErrorCorrectionLevel;
|
||||||
|
|
||||||
|
final class ErrorCorrectionLevelLow implements ErrorCorrectionLevelInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
9
vendor/endroid/qr-code/src/ErrorCorrectionLevel/ErrorCorrectionLevelMedium.php
vendored
Normal file
9
vendor/endroid/qr-code/src/ErrorCorrectionLevel/ErrorCorrectionLevelMedium.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\ErrorCorrectionLevel;
|
||||||
|
|
||||||
|
final class ErrorCorrectionLevelMedium implements ErrorCorrectionLevelInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
9
vendor/endroid/qr-code/src/ErrorCorrectionLevel/ErrorCorrectionLevelQuartile.php
vendored
Normal file
9
vendor/endroid/qr-code/src/ErrorCorrectionLevel/ErrorCorrectionLevelQuartile.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\ErrorCorrectionLevel;
|
||||||
|
|
||||||
|
final class ErrorCorrectionLevelQuartile implements ErrorCorrectionLevelInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\Label\Alignment;
|
||||||
|
|
||||||
|
final class LabelAlignmentCenter implements LabelAlignmentInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\Label\Alignment;
|
||||||
|
|
||||||
|
interface LabelAlignmentInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\Label\Alignment;
|
||||||
|
|
||||||
|
final class LabelAlignmentLeft implements LabelAlignmentInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\Label\Alignment;
|
||||||
|
|
||||||
|
final class LabelAlignmentRight implements LabelAlignmentInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\Label\Font;
|
||||||
|
|
||||||
|
interface FontInterface
|
||||||
|
{
|
||||||
|
public function getPath(): string;
|
||||||
|
|
||||||
|
public function getSize(): int;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\RoundBlockSizeMode;
|
||||||
|
|
||||||
|
final class RoundBlockSizeModeEnlarge implements RoundBlockSizeModeInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
9
vendor/endroid/qr-code/src/RoundBlockSizeMode/RoundBlockSizeModeInterface.php
vendored
Normal file
9
vendor/endroid/qr-code/src/RoundBlockSizeMode/RoundBlockSizeModeInterface.php
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\RoundBlockSizeMode;
|
||||||
|
|
||||||
|
interface RoundBlockSizeModeInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\RoundBlockSizeMode;
|
||||||
|
|
||||||
|
final class RoundBlockSizeModeMargin implements RoundBlockSizeModeInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\RoundBlockSizeMode;
|
||||||
|
|
||||||
|
final class RoundBlockSizeModeNone implements RoundBlockSizeModeInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Endroid\QrCode\RoundBlockSizeMode;
|
||||||
|
|
||||||
|
final class RoundBlockSizeModeShrink implements RoundBlockSizeModeInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
interface WritableInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
|
||||||
|
if ($options[self::WRITER_OPTION_FORCE_XLINK_HREF]) {
|
||||||
|
$imageDefinition->addAttribute('xlink:href', $logoImageData->createDataUri(), 'http://www.w3.org/1999/xlink');
|
||||||
|
} else {
|
||||||
|
$imageDefinition->addAttribute('href', $logoImageData->createDataUri());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getOpacity(int $alpha): float
|
|
||||||
{
|
|
||||||
$opacity = 1 - $alpha / 127;
|
|
||||||
|
|
||||||
return $opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getContentType(): string
|
|
||||||
{
|
|
||||||
return 'image/svg+xml';
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getSupportedExtensions(): array
|
|
||||||
{
|
|
||||||
return ['svg'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return 'svg';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace GuzzleHttp\Exception;
|
||||||
|
|
||||||
|
final class InvalidArgumentException extends \InvalidArgumentException implements GuzzleException
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
@ -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')) {
|
||||||
|
|
@ -388,7 +409,7 @@ class CurlFactory implements CurlFactoryInterface
|
||||||
if (isset($options['force_ip_resolve'])) {
|
if (isset($options['force_ip_resolve'])) {
|
||||||
if ('v4' === $options['force_ip_resolve']) {
|
if ('v4' === $options['force_ip_resolve']) {
|
||||||
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
|
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
|
||||||
} else if ('v6' === $options['force_ip_resolve']) {
|
} elseif ('v6' === $options['force_ip_resolve']) {
|
||||||
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
|
$conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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}"
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
@ -103,7 +104,7 @@ class StreamHandler
|
||||||
$status = $parts[1];
|
$status = $parts[1];
|
||||||
$reason = isset($parts[2]) ? $parts[2] : null;
|
$reason = isset($parts[2]) ? $parts[2] : null;
|
||||||
$headers = \GuzzleHttp\headers_from_lines($hdrs);
|
$headers = \GuzzleHttp\headers_from_lines($hdrs);
|
||||||
list ($stream, $headers) = $this->checkDecode($options, $headers, $stream);
|
list($stream, $headers) = $this->checkDecode($options, $headers, $stream);
|
||||||
$stream = Psr7\stream_for($stream);
|
$stream = Psr7\stream_for($stream);
|
||||||
$sink = $stream;
|
$sink = $stream;
|
||||||
|
|
||||||
|
|
@ -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'] . ']');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
github: [Nyholm, GrahamCampbell]
|
||||||
|
tidelift: "packagist/guzzlehttp/psr7"
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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]++)";
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
vendor
|
||||||
|
build/artifacts/
|
||||||
|
composer.lock
|
||||||
|
docs/_build/
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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."
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
]);
|
||||||
|
|
@ -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
Loading…
Reference in New Issue