271 changed files with 35066 additions and 0 deletions
@ -0,0 +1 @@ |
|||
!.gitignore |
|||
@ -0,0 +1,25 @@ |
|||
<?php |
|||
|
|||
// autoload.php @generated by Composer |
|||
|
|||
if (PHP_VERSION_ID < 50600) { |
|||
if (!headers_sent()) { |
|||
header('HTTP/1.1 500 Internal Server Error'); |
|||
} |
|||
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; |
|||
if (!ini_get('display_errors')) { |
|||
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { |
|||
fwrite(STDERR, $err); |
|||
} elseif (!headers_sent()) { |
|||
echo $err; |
|||
} |
|||
} |
|||
trigger_error( |
|||
$err, |
|||
E_USER_ERROR |
|||
); |
|||
} |
|||
|
|||
require_once __DIR__ . '/composer/autoload_real.php'; |
|||
|
|||
return ComposerAutoloaderInitb17d9cf9df517c3dd2f492a1c933c315::getLoader(); |
|||
@ -0,0 +1 @@ |
|||
open_collective: zipstream |
|||
@ -0,0 +1,12 @@ |
|||
# Description of the problem |
|||
|
|||
Please be very descriptive and include as much details as possible. |
|||
|
|||
# Example code |
|||
|
|||
# Informations |
|||
|
|||
* ZipStream-PHP version: |
|||
* PHP version: |
|||
|
|||
Please include any supplemental information you deem relevant to this issue. |
|||
@ -0,0 +1,6 @@ |
|||
clover.xml |
|||
composer.lock |
|||
coverage.clover |
|||
.idea |
|||
phpunit.xml |
|||
vendor |
|||
@ -0,0 +1,12 @@ |
|||
language: php |
|||
dist: trusty |
|||
sudo: false |
|||
php: |
|||
- 7.1 |
|||
- 7.2 |
|||
- 7.3 |
|||
install: composer install |
|||
script: ./vendor/bin/phpunit --coverage-clover=coverage.clover |
|||
after_script: |
|||
- wget https://scrutinizer-ci.com/ocular.phar |
|||
- php ocular.phar code-coverage:upload --format=php-clover coverage.clover |
|||
@ -0,0 +1,51 @@ |
|||
# CHANGELOG for ZipStream-PHP |
|||
|
|||
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). |
|||
|
|||
## [2.1.0] - 2020-06-01 |
|||
### Changed |
|||
- Don't execute ob_flush() when output buffering is not enabled (#152) |
|||
- Fix inconsistent return type on 32-bit systems (#149) Fix #144 |
|||
- Use mbstring polyfill (#151) |
|||
- Promote 7zip usage over unzip to avoid UTF-8 issues (#147) |
|||
|
|||
## [2.0.0] - 2020-02-22 |
|||
### Breaking change |
|||
- Only the self opened streams will be closed (#139) |
|||
If you were relying on ZipStream to close streams that the library didn't open, |
|||
you'll need to close them yourself now. |
|||
|
|||
### Changed |
|||
- Minor change to data descriptor (#136) |
|||
|
|||
## [1.2.0] - 2019-07-11 |
|||
|
|||
### Added |
|||
- Option to flush output buffer after every write (#122) |
|||
|
|||
## [1.1.0] - 2019-04-30 |
|||
|
|||
### Fixed |
|||
- Honor last-modified timestamps set via `ZipStream\Option\File::setTime()` (#106) |
|||
- Documentation regarding output of HTTP headers |
|||
- Test warnings with PHPUnit (#109) |
|||
|
|||
### Added |
|||
- Test for FileNotReadableException (#114) |
|||
- Size attribute to File options (#113) |
|||
- Tests on PHP 7.3 (#108) |
|||
|
|||
## [1.0.0] - 2019-04-17 |
|||
|
|||
### Breaking changes |
|||
- Mininum PHP version is now 7.1 |
|||
- Options are now passed to the ZipStream object via the Option\Archive object. See the wiki for available options and code examples |
|||
|
|||
### Added |
|||
- Add large file support with Zip64 headers |
|||
|
|||
### Changed |
|||
- Major refactoring and code cleanup |
|||
@ -0,0 +1,25 @@ |
|||
# ZipStream Readme for Contributors |
|||
## Code styling |
|||
### Indention |
|||
For spaces are used to indent code. The convention is [K&R](http://en.wikipedia.org/wiki/Indent_style#K&R) |
|||
|
|||
### Comments |
|||
Double Slashes are used for an one line comment. |
|||
|
|||
Classes, Variables, Methods etc: |
|||
|
|||
```php |
|||
/** |
|||
* My comment |
|||
* |
|||
* @myanotation like @param etc. |
|||
*/ |
|||
``` |
|||
|
|||
## Pull requests |
|||
Feel free to submit pull requests. |
|||
|
|||
## Testing |
|||
For every new feature please write a new PHPUnit test. |
|||
|
|||
Before every commit execute `./vendor/bin/phpunit` to check if your changes wrecked something: |
|||
@ -0,0 +1,24 @@ |
|||
MIT License |
|||
|
|||
Copyright (C) 2007-2009 Paul Duncan <pabs@pablotron.org> |
|||
Copyright (C) 2014 Jonatan Männchen <jonatan@maennchen.ch> |
|||
Copyright (C) 2014 Jesse G. Donat <donatj@gmail.com> |
|||
Copyright (C) 2018 Nicolas CARPi <nicolas.carpi@curie.fr> |
|||
|
|||
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,123 @@ |
|||
# ZipStream-PHP |
|||
|
|||
[](https://travis-ci.org/maennchen/ZipStream-PHP) |
|||
[](https://scrutinizer-ci.com/g/maennchen/ZipStream-PHP/) |
|||
[](https://scrutinizer-ci.com/g/maennchen/ZipStream-PHP/) |
|||
[](https://packagist.org/packages/maennchen/zipstream-php) |
|||
[](https://packagist.org/packages/maennchen/zipstream-php) |
|||
[](https://opencollective.com/zipstream) [](LICENSE) |
|||
|
|||
## Overview |
|||
|
|||
A fast and simple streaming zip file downloader for PHP. Using this library will save you from having to write the Zip to disk. You can directly send it to the user, which is much faster. It can work with S3 buckets or any PSR7 Stream. |
|||
|
|||
Please see the [LICENSE](LICENSE) file for licensing and warranty information. |
|||
|
|||
## Installation |
|||
|
|||
Simply add a dependency on maennchen/zipstream-php to your project's composer.json file if you use Composer to manage the dependencies of your project. Use following command to add the package to your project's dependencies: |
|||
|
|||
```bash |
|||
composer require maennchen/zipstream-php |
|||
``` |
|||
|
|||
## Usage and options |
|||
|
|||
Here's a simple example: |
|||
|
|||
```php |
|||
// Autoload the dependencies |
|||
require 'vendor/autoload.php'; |
|||
|
|||
// enable output of HTTP headers |
|||
$options = new ZipStream\Option\Archive(); |
|||
$options->setSendHttpHeaders(true); |
|||
|
|||
// create a new zipstream object |
|||
$zip = new ZipStream\ZipStream('example.zip', $options); |
|||
|
|||
// create a file named 'hello.txt' |
|||
$zip->addFile('hello.txt', 'This is the contents of hello.txt'); |
|||
|
|||
// add a file named 'some_image.jpg' from a local file 'path/to/image.jpg' |
|||
$zip->addFileFromPath('some_image.jpg', 'path/to/image.jpg'); |
|||
|
|||
// add a file named 'goodbye.txt' from an open stream resource |
|||
$fp = tmpfile(); |
|||
fwrite($fp, 'The quick brown fox jumped over the lazy dog.'); |
|||
rewind($fp); |
|||
$zip->addFileFromStream('goodbye.txt', $fp); |
|||
fclose($fp); |
|||
|
|||
// finish the zip stream |
|||
$zip->finish(); |
|||
``` |
|||
|
|||
You can also add comments, modify file timestamps, and customize (or |
|||
disable) the HTTP headers. It is also possible to specify the storage method when adding files, |
|||
the current default storage method is 'deflate' i.e files are stored with Compression mode 0x08. |
|||
|
|||
See the [Wiki](https://github.com/maennchen/ZipStream-PHP/wiki) for details. |
|||
|
|||
## Known issue |
|||
|
|||
The native Mac OS archive extraction tool might not open archives in some conditions. A workaround is to disable the Zip64 feature with the option `$opt->setEnableZip64(false)`. This limits the archive to 4 Gb and 64k files but will allow Mac OS users to open them without issue. See #116. |
|||
|
|||
The linux `unzip` utility might not handle properly unicode characters. It is recommended to extract with another tool like [7-zip](https://www.7-zip.org/). See #146. |
|||
|
|||
## Upgrade to version 2.0.0 |
|||
|
|||
* Only the self opened streams will be closed (#139) |
|||
If you were relying on ZipStream to close streams that the library didn't open, |
|||
you'll need to close them yourself now. |
|||
|
|||
## Upgrade to version 1.0.0 |
|||
|
|||
* All options parameters to all function have been moved from an `array` to structured option objects. See [the wiki](https://github.com/maennchen/ZipStream-PHP/wiki/Available-options) for examples. |
|||
* The whole library has been refactored. The minimal PHP requirement has been raised to PHP 7.1. |
|||
|
|||
## Usage with Symfony and S3 |
|||
|
|||
You can find example code on [the wiki](https://github.com/maennchen/ZipStream-PHP/wiki/Symfony-example). |
|||
|
|||
## Contributing |
|||
|
|||
ZipStream-PHP is a collaborative project. Please take a look at the [CONTRIBUTING.md](CONTRIBUTING.md) file. |
|||
|
|||
## About the Authors |
|||
|
|||
* Paul Duncan <pabs@pablotron.org> - https://pablotron.org/ |
|||
* Jonatan Männchen <jonatan@maennchen.ch> - https://maennchen.dev |
|||
* Jesse G. Donat <donatj@gmail.com> - https://donatstudios.com |
|||
* Nicolas CARPi <nico-git@deltablot.email> - https://www.deltablot.com |
|||
* Nik Barham <nik@brokencube.co.uk> - https://www.brokencube.co.uk |
|||
|
|||
## Contributors |
|||
|
|||
### Code Contributors |
|||
|
|||
This project exists thanks to all the people who contribute. [[Contribute](CONTRIBUTING.md)]. |
|||
<a href="https://github.com/maennchen/ZipStream-PHP/graphs/contributors"><img src="https://opencollective.com/zipstream/contributors.svg?width=890&button=false" /></a> |
|||
|
|||
### Financial Contributors |
|||
|
|||
Become a financial contributor and help us sustain our community. [[Contribute](https://opencollective.com/zipstream/contribute)] |
|||
|
|||
#### Individuals |
|||
|
|||
<a href="https://opencollective.com/zipstream"><img src="https://opencollective.com/zipstream/individuals.svg?width=890"></a> |
|||
|
|||
#### Organizations |
|||
|
|||
Support this project with your organization. Your logo will show up here with a link to your website. [[Contribute](https://opencollective.com/zipstream/contribute)] |
|||
|
|||
<a href="https://opencollective.com/zipstream/organization/0/website"><img src="https://opencollective.com/zipstream/organization/0/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/1/website"><img src="https://opencollective.com/zipstream/organization/1/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/2/website"><img src="https://opencollective.com/zipstream/organization/2/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/3/website"><img src="https://opencollective.com/zipstream/organization/3/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/4/website"><img src="https://opencollective.com/zipstream/organization/4/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/5/website"><img src="https://opencollective.com/zipstream/organization/5/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/6/website"><img src="https://opencollective.com/zipstream/organization/6/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/7/website"><img src="https://opencollective.com/zipstream/organization/7/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/8/website"><img src="https://opencollective.com/zipstream/organization/8/avatar.svg"></a> |
|||
<a href="https://opencollective.com/zipstream/organization/9/website"><img src="https://opencollective.com/zipstream/organization/9/avatar.svg"></a> |
|||
@ -0,0 +1,41 @@ |
|||
{ |
|||
"name": "maennchen/zipstream-php", |
|||
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", |
|||
"keywords": ["zip", "stream"], |
|||
"type": "library", |
|||
"license": "MIT", |
|||
"authors": [{ |
|||
"name": "Paul Duncan", |
|||
"email": "pabs@pablotron.org" |
|||
}, |
|||
{ |
|||
"name": "Jonatan Männchen", |
|||
"email": "jonatan@maennchen.ch" |
|||
}, |
|||
{ |
|||
"name": "Jesse Donat", |
|||
"email": "donatj@gmail.com" |
|||
}, |
|||
{ |
|||
"name": "András Kolesár", |
|||
"email": "kolesar@kolesar.hu" |
|||
} |
|||
], |
|||
"require": { |
|||
"php": ">= 7.1", |
|||
"symfony/polyfill-mbstring": "^1.0", |
|||
"psr/http-message": "^1.0", |
|||
"myclabs/php-enum": "^1.5" |
|||
}, |
|||
"require-dev": { |
|||
"phpunit/phpunit": ">= 7.5", |
|||
"guzzlehttp/guzzle": ">= 6.3", |
|||
"ext-zip": "*", |
|||
"mikey179/vfsstream": "^1.6" |
|||
}, |
|||
"autoload": { |
|||
"psr-4": { |
|||
"ZipStream\\": "src/" |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
<phpunit bootstrap="test/bootstrap.php"> |
|||
<testsuites> |
|||
<testsuite name="Application"> |
|||
<directory>test</directory> |
|||
</testsuite> |
|||
</testsuites> |
|||
|
|||
<logging> |
|||
<log type="coverage-clover" target="clover.xml"/> |
|||
</logging> |
|||
|
|||
<filter> |
|||
<whitelist processUncoveredFilesFromWhitelist="true"> |
|||
<directory suffix=".php">src</directory> |
|||
</whitelist> |
|||
</filter> |
|||
</phpunit> |
|||
@ -0,0 +1,55 @@ |
|||
<?xml version="1.0"?> |
|||
<psalm |
|||
totallyTyped="false" |
|||
resolveFromConfigFile="true" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xmlns="https://getpsalm.org/schema/config" |
|||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" |
|||
> |
|||
<projectFiles> |
|||
<directory name="src" /> |
|||
<ignoreFiles> |
|||
<directory name="vendor" /> |
|||
</ignoreFiles> |
|||
</projectFiles> |
|||
|
|||
<issueHandlers> |
|||
<LessSpecificReturnType errorLevel="info" /> |
|||
|
|||
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives --> |
|||
|
|||
<DeprecatedMethod errorLevel="info" /> |
|||
<DeprecatedProperty errorLevel="info" /> |
|||
<DeprecatedClass errorLevel="info" /> |
|||
<DeprecatedConstant errorLevel="info" /> |
|||
<DeprecatedFunction errorLevel="info" /> |
|||
<DeprecatedInterface errorLevel="info" /> |
|||
<DeprecatedTrait errorLevel="info" /> |
|||
|
|||
<InternalMethod errorLevel="info" /> |
|||
<InternalProperty errorLevel="info" /> |
|||
<InternalClass errorLevel="info" /> |
|||
|
|||
<MissingClosureReturnType errorLevel="info" /> |
|||
<MissingReturnType errorLevel="info" /> |
|||
<MissingPropertyType errorLevel="info" /> |
|||
<InvalidDocblock errorLevel="info" /> |
|||
<MisplacedRequiredParam errorLevel="info" /> |
|||
|
|||
<PropertyNotSetInConstructor errorLevel="info" /> |
|||
<MissingConstructor errorLevel="info" /> |
|||
<MissingClosureParamType errorLevel="info" /> |
|||
<MissingParamType errorLevel="info" /> |
|||
|
|||
<RedundantCondition errorLevel="info" /> |
|||
|
|||
<DocblockTypeContradiction errorLevel="info" /> |
|||
<RedundantConditionGivenDocblockType errorLevel="info" /> |
|||
|
|||
<UnresolvableInclude errorLevel="info" /> |
|||
|
|||
<RawObjectIteration errorLevel="info" /> |
|||
|
|||
<InvalidStringClass errorLevel="info" /> |
|||
</issueHandlers> |
|||
</psalm> |
|||
@ -0,0 +1,172 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream; |
|||
|
|||
use OverflowException; |
|||
|
|||
class Bigint |
|||
{ |
|||
/** |
|||
* @var int[] |
|||
*/ |
|||
private $bytes = [0, 0, 0, 0, 0, 0, 0, 0]; |
|||
|
|||
/** |
|||
* Initialize the bytes array |
|||
* |
|||
* @param int $value |
|||
*/ |
|||
public function __construct(int $value = 0) |
|||
{ |
|||
$this->fillBytes($value, 0, 8); |
|||
} |
|||
|
|||
/** |
|||
* Fill the bytes field with int |
|||
* |
|||
* @param int $value |
|||
* @param int $start |
|||
* @param int $count |
|||
* @return void |
|||
*/ |
|||
protected function fillBytes(int $value, int $start, int $count): void |
|||
{ |
|||
for ($i = 0; $i < $count; $i++) { |
|||
$this->bytes[$start + $i] = $i >= PHP_INT_SIZE ? 0 : $value & 0xFF; |
|||
$value >>= 8; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Get an instance |
|||
* |
|||
* @param int $value |
|||
* @return Bigint |
|||
*/ |
|||
public static function init(int $value = 0): self |
|||
{ |
|||
return new self($value); |
|||
} |
|||
|
|||
/** |
|||
* Fill bytes from low to high |
|||
* |
|||
* @param int $low |
|||
* @param int $high |
|||
* @return Bigint |
|||
*/ |
|||
public static function fromLowHigh(int $low, int $high): self |
|||
{ |
|||
$bigint = new Bigint(); |
|||
$bigint->fillBytes($low, 0, 4); |
|||
$bigint->fillBytes($high, 4, 4); |
|||
return $bigint; |
|||
} |
|||
|
|||
/** |
|||
* Get high 32 |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getHigh32(): int |
|||
{ |
|||
return $this->getValue(4, 4); |
|||
} |
|||
|
|||
/** |
|||
* Get value from bytes array |
|||
* |
|||
* @param int $end |
|||
* @param int $length |
|||
* @return int |
|||
*/ |
|||
public function getValue(int $end = 0, int $length = 8): int |
|||
{ |
|||
$result = 0; |
|||
for ($i = $end + $length - 1; $i >= $end; $i--) { |
|||
$result <<= 8; |
|||
$result |= $this->bytes[$i]; |
|||
} |
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Get low FF |
|||
* |
|||
* @param bool $force |
|||
* @return float |
|||
*/ |
|||
public function getLowFF(bool $force = false): float |
|||
{ |
|||
if ($force || $this->isOver32()) { |
|||
return (float)0xFFFFFFFF; |
|||
} |
|||
return (float)$this->getLow32(); |
|||
} |
|||
|
|||
/** |
|||
* Check if is over 32 |
|||
* |
|||
* @param bool $force |
|||
* @return bool |
|||
*/ |
|||
public function isOver32(bool $force = false): bool |
|||
{ |
|||
// value 0xFFFFFFFF already needs a Zip64 header |
|||
return $force || |
|||
max(array_slice($this->bytes, 4, 4)) > 0 || |
|||
min(array_slice($this->bytes, 0, 4)) === 0xFF; |
|||
} |
|||
|
|||
/** |
|||
* Get low 32 |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getLow32(): int |
|||
{ |
|||
return $this->getValue(0, 4); |
|||
} |
|||
|
|||
/** |
|||
* Get hexadecimal |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getHex64(): string |
|||
{ |
|||
$result = '0x'; |
|||
for ($i = 7; $i >= 0; $i--) { |
|||
$result .= sprintf('%02X', $this->bytes[$i]); |
|||
} |
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Add |
|||
* |
|||
* @param Bigint $other |
|||
* @return Bigint |
|||
*/ |
|||
public function add(Bigint $other): Bigint |
|||
{ |
|||
$result = clone $this; |
|||
$overflow = false; |
|||
for ($i = 0; $i < 8; $i++) { |
|||
$result->bytes[$i] += $other->bytes[$i]; |
|||
if ($overflow) { |
|||
$result->bytes[$i]++; |
|||
$overflow = false; |
|||
} |
|||
if ($result->bytes[$i] & 0x100) { |
|||
$overflow = true; |
|||
$result->bytes[$i] &= 0xFF; |
|||
} |
|||
} |
|||
if ($overflow) { |
|||
throw new OverflowException; |
|||
} |
|||
return $result; |
|||
} |
|||
} |
|||
@ -0,0 +1,70 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream; |
|||
|
|||
class DeflateStream extends Stream |
|||
{ |
|||
protected $filter; |
|||
|
|||
/** |
|||
* @var Option\File |
|||
*/ |
|||
protected $options; |
|||
|
|||
/** |
|||
* Rewind stream |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function rewind(): void |
|||
{ |
|||
// deflate filter needs to be removed before rewind |
|||
if ($this->filter) { |
|||
$this->removeDeflateFilter(); |
|||
$this->seek(0); |
|||
$this->addDeflateFilter($this->options); |
|||
} else { |
|||
rewind($this->stream); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Remove the deflate filter |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function removeDeflateFilter(): void |
|||
{ |
|||
if (!$this->filter) { |
|||
return; |
|||
} |
|||
stream_filter_remove($this->filter); |
|||
$this->filter = null; |
|||
} |
|||
|
|||
/** |
|||
* Add a deflate filter |
|||
* |
|||
* @param Option\File $options |
|||
* @return void |
|||
*/ |
|||
public function addDeflateFilter(Option\File $options): void |
|||
{ |
|||
$this->options = $options; |
|||
// parameter 4 for stream_filter_append expects array |
|||
// so we convert the option object in an array |
|||
$optionsArr = [ |
|||
'comment' => $options->getComment(), |
|||
'method' => $options->getMethod(), |
|||
'deflateLevel' => $options->getDeflateLevel(), |
|||
'time' => $options->getTime() |
|||
]; |
|||
$this->filter = stream_filter_append( |
|||
$this->stream, |
|||
'zlib.deflate', |
|||
STREAM_FILTER_READ, |
|||
$optionsArr |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream; |
|||
|
|||
/** |
|||
* This class is only for inheriting |
|||
*/ |
|||
abstract class Exception extends \Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Exception; |
|||
|
|||
use ZipStream\Exception; |
|||
|
|||
/** |
|||
* This Exception gets invoked if file or comment encoding is incorrect |
|||
*/ |
|||
class EncodingException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Exception; |
|||
|
|||
use ZipStream\Exception; |
|||
|
|||
/** |
|||
* This Exception gets invoked if a file wasn't found |
|||
*/ |
|||
class FileNotFoundException extends Exception |
|||
{ |
|||
/** |
|||
* Constructor of the Exception |
|||
* |
|||
* @param String $path - The path which wasn't found |
|||
*/ |
|||
public function __construct(string $path) |
|||
{ |
|||
parent::__construct("The file with the path $path wasn't found."); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Exception; |
|||
|
|||
use ZipStream\Exception; |
|||
|
|||
/** |
|||
* This Exception gets invoked if a file wasn't found |
|||
*/ |
|||
class FileNotReadableException extends Exception |
|||
{ |
|||
/** |
|||
* Constructor of the Exception |
|||
* |
|||
* @param String $path - The path which wasn't found |
|||
*/ |
|||
public function __construct(string $path) |
|||
{ |
|||
parent::__construct("The file with the path $path isn't readable."); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Exception; |
|||
|
|||
use ZipStream\Exception; |
|||
|
|||
/** |
|||
* This Exception gets invoked if options are incompatible |
|||
*/ |
|||
class IncompatibleOptionsException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Exception; |
|||
|
|||
use ZipStream\Exception; |
|||
|
|||
/** |
|||
* This Exception gets invoked if a counter value exceeds storage size |
|||
*/ |
|||
class OverflowException extends Exception |
|||
{ |
|||
public function __construct() |
|||
{ |
|||
parent::__construct('File size exceeds limit of 32 bit integer. Please enable "zip64" option.'); |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Exception; |
|||
|
|||
use ZipStream\Exception; |
|||
|
|||
/** |
|||
* This Exception gets invoked if `fread` fails on a stream. |
|||
*/ |
|||
class StreamNotReadableException extends Exception |
|||
{ |
|||
/** |
|||
* Constructor of the Exception |
|||
* |
|||
* @param string $fileName - The name of the file which the stream belongs to. |
|||
*/ |
|||
public function __construct(string $fileName) |
|||
{ |
|||
parent::__construct("The stream for $fileName could not be read."); |
|||
} |
|||
} |
|||
@ -0,0 +1,477 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream; |
|||
|
|||
use Psr\Http\Message\StreamInterface; |
|||
use ZipStream\Exception\EncodingException; |
|||
use ZipStream\Exception\FileNotFoundException; |
|||
use ZipStream\Exception\FileNotReadableException; |
|||
use ZipStream\Exception\OverflowException; |
|||
use ZipStream\Option\File as FileOptions; |
|||
use ZipStream\Option\Method; |
|||
use ZipStream\Option\Version; |
|||
|
|||
class File |
|||
{ |
|||
const HASH_ALGORITHM = 'crc32b'; |
|||
|
|||
const BIT_ZERO_HEADER = 0x0008; |
|||
const BIT_EFS_UTF8 = 0x0800; |
|||
|
|||
const COMPUTE = 1; |
|||
const SEND = 2; |
|||
|
|||
private const CHUNKED_READ_BLOCK_SIZE = 1048576; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
public $name; |
|||
|
|||
/** |
|||
* @var FileOptions |
|||
*/ |
|||
public $opt; |
|||
|
|||
/** |
|||
* @var Bigint |
|||
*/ |
|||
public $len; |
|||
/** |
|||
* @var Bigint |
|||
*/ |
|||
public $zlen; |
|||
|
|||
/** @var int */ |
|||
public $crc; |
|||
|
|||
/** |
|||
* @var Bigint |
|||
*/ |
|||
public $hlen; |
|||
|
|||
/** |
|||
* @var Bigint |
|||
*/ |
|||
public $ofs; |
|||
|
|||
/** |
|||
* @var int |
|||
*/ |
|||
public $bits; |
|||
|
|||
/** |
|||
* @var Version |
|||
*/ |
|||
public $version; |
|||
|
|||
/** |
|||
* @var ZipStream |
|||
*/ |
|||
public $zip; |
|||
|
|||
/** |
|||
* @var resource |
|||
*/ |
|||
private $deflate; |
|||
/** |
|||
* @var resource |
|||
*/ |
|||
private $hash; |
|||
|
|||
/** |
|||
* @var Method |
|||
*/ |
|||
private $method; |
|||
|
|||
/** |
|||
* @var Bigint |
|||
*/ |
|||
private $totalLength; |
|||
|
|||
public function __construct(ZipStream $zip, string $name, ?FileOptions $opt = null) |
|||
{ |
|||
$this->zip = $zip; |
|||
|
|||
$this->name = $name; |
|||
$this->opt = $opt ?: new FileOptions(); |
|||
$this->method = $this->opt->getMethod(); |
|||
$this->version = Version::STORE(); |
|||
$this->ofs = new Bigint(); |
|||
} |
|||
|
|||
public function processPath(string $path): void |
|||
{ |
|||
if (!is_readable($path)) { |
|||
if (!file_exists($path)) { |
|||
throw new FileNotFoundException($path); |
|||
} |
|||
throw new FileNotReadableException($path); |
|||
} |
|||
if ($this->zip->isLargeFile($path) === false) { |
|||
$data = file_get_contents($path); |
|||
$this->processData($data); |
|||
} else { |
|||
$this->method = $this->zip->opt->getLargeFileMethod(); |
|||
|
|||
$stream = new DeflateStream(fopen($path, 'rb')); |
|||
$this->processStream($stream); |
|||
$stream->close(); |
|||
} |
|||
} |
|||
|
|||
public function processData(string $data): void |
|||
{ |
|||
$this->len = new Bigint(strlen($data)); |
|||
$this->crc = crc32($data); |
|||
|
|||
// compress data if needed |
|||
if ($this->method->equals(Method::DEFLATE())) { |
|||
$data = gzdeflate($data); |
|||
} |
|||
|
|||
$this->zlen = new Bigint(strlen($data)); |
|||
$this->addFileHeader(); |
|||
$this->zip->send($data); |
|||
$this->addFileFooter(); |
|||
} |
|||
|
|||
/** |
|||
* Create and send zip header for this file. |
|||
* |
|||
* @return void |
|||
* @throws \ZipStream\Exception\EncodingException |
|||
*/ |
|||
public function addFileHeader(): void |
|||
{ |
|||
$name = static::filterFilename($this->name); |
|||
|
|||
// calculate name length |
|||
$nameLength = strlen($name); |
|||
|
|||
// create dos timestamp |
|||
$time = static::dosTime($this->opt->getTime()->getTimestamp()); |
|||
|
|||
$comment = $this->opt->getComment(); |
|||
|
|||
if (!mb_check_encoding($name, 'ASCII') || |
|||
!mb_check_encoding($comment, 'ASCII')) { |
|||
// Sets Bit 11: Language encoding flag (EFS). If this bit is set, |
|||
// the filename and comment fields for this file |
|||
// MUST be encoded using UTF-8. (see APPENDIX D) |
|||
if (!mb_check_encoding($name, 'UTF-8') || |
|||
!mb_check_encoding($comment, 'UTF-8')) { |
|||
throw new EncodingException( |
|||
'File name and comment should use UTF-8 ' . |
|||
'if one of them does not fit into ASCII range.' |
|||
); |
|||
} |
|||
$this->bits |= self::BIT_EFS_UTF8; |
|||
} |
|||
|
|||
if ($this->method->equals(Method::DEFLATE())) { |
|||
$this->version = Version::DEFLATE(); |
|||
} |
|||
|
|||
$force = (boolean)($this->bits & self::BIT_ZERO_HEADER) && |
|||
$this->zip->opt->isEnableZip64(); |
|||
|
|||
$footer = $this->buildZip64ExtraBlock($force); |
|||
|
|||
// If this file will start over 4GB limit in ZIP file, |
|||
// CDR record will have to use Zip64 extension to describe offset |
|||
// to keep consistency we use the same value here |
|||
if ($this->zip->ofs->isOver32()) { |
|||
$this->version = Version::ZIP64(); |
|||
} |
|||
|
|||
$fields = [ |
|||
['V', ZipStream::FILE_HEADER_SIGNATURE], |
|||
['v', $this->version->getValue()], // Version needed to Extract |
|||
['v', $this->bits], // General purpose bit flags - data descriptor flag set |
|||
['v', $this->method->getValue()], // Compression method |
|||
['V', $time], // Timestamp (DOS Format) |
|||
['V', $this->crc], // CRC32 of data (0 -> moved to data descriptor footer) |
|||
['V', $this->zlen->getLowFF($force)], // Length of compressed data (forced to 0xFFFFFFFF for zero header) |
|||
['V', $this->len->getLowFF($force)], // Length of original data (forced to 0xFFFFFFFF for zero header) |
|||
['v', $nameLength], // Length of filename |
|||
['v', strlen($footer)], // Extra data (see above) |
|||
]; |
|||
|
|||
// pack fields and calculate "total" length |
|||
$header = ZipStream::packFields($fields); |
|||
|
|||
// print header and filename |
|||
$data = $header . $name . $footer; |
|||
$this->zip->send($data); |
|||
|
|||
// save header length |
|||
$this->hlen = Bigint::init(strlen($data)); |
|||
} |
|||
|
|||
/** |
|||
* Strip characters that are not legal in Windows filenames |
|||
* to prevent compatibility issues |
|||
* |
|||
* @param string $filename Unprocessed filename |
|||
* @return string |
|||
*/ |
|||
public static function filterFilename(string $filename): string |
|||
{ |
|||
// strip leading slashes from file name |
|||
// (fixes bug in windows archive viewer) |
|||
$filename = preg_replace('/^\\/+/', '', $filename); |
|||
|
|||
return str_replace(['\\', ':', '*', '?', '"', '<', '>', '|'], '_', $filename); |
|||
} |
|||
|
|||
/** |
|||
* Convert a UNIX timestamp to a DOS timestamp. |
|||
* |
|||
* @param int $when |
|||
* @return int DOS Timestamp |
|||
*/ |
|||
final protected static function dosTime(int $when): int |
|||
{ |
|||
// get date array for timestamp |
|||
$d = getdate($when); |
|||
|
|||
// set lower-bound on dates |
|||
if ($d['year'] < 1980) { |
|||
$d = array( |
|||
'year' => 1980, |
|||
'mon' => 1, |
|||
'mday' => 1, |
|||
'hours' => 0, |
|||
'minutes' => 0, |
|||
'seconds' => 0 |
|||
); |
|||
} |
|||
|
|||
// remove extra years from 1980 |
|||
$d['year'] -= 1980; |
|||
|
|||
// return date string |
|||
return |
|||
($d['year'] << 25) | |
|||
($d['mon'] << 21) | |
|||
($d['mday'] << 16) | |
|||
($d['hours'] << 11) | |
|||
($d['minutes'] << 5) | |
|||
($d['seconds'] >> 1); |
|||
} |
|||
|
|||
protected function buildZip64ExtraBlock(bool $force = false): string |
|||
{ |
|||
|
|||
$fields = []; |
|||
if ($this->len->isOver32($force)) { |
|||
$fields[] = ['P', $this->len]; // Length of original data |
|||
} |
|||
|
|||
if ($this->len->isOver32($force)) { |
|||
$fields[] = ['P', $this->zlen]; // Length of compressed data |
|||
} |
|||
|
|||
if ($this->ofs->isOver32()) { |
|||
$fields[] = ['P', $this->ofs]; // Offset of local header record |
|||
} |
|||
|
|||
if (!empty($fields)) { |
|||
if (!$this->zip->opt->isEnableZip64()) { |
|||
throw new OverflowException(); |
|||
} |
|||
|
|||
array_unshift( |
|||
$fields, |
|||
['v', 0x0001], // 64 bit extension |
|||
['v', count($fields) * 8] // Length of data block |
|||
); |
|||
$this->version = Version::ZIP64(); |
|||
} |
|||
|
|||
return ZipStream::packFields($fields); |
|||
} |
|||
|
|||
/** |
|||
* Create and send data descriptor footer for this file. |
|||
* |
|||
* @return void |
|||
*/ |
|||
|
|||
public function addFileFooter(): void |
|||
{ |
|||
|
|||
if ($this->bits & self::BIT_ZERO_HEADER) { |
|||
// compressed and uncompressed size |
|||
$sizeFormat = 'V'; |
|||
if ($this->zip->opt->isEnableZip64()) { |
|||
$sizeFormat = 'P'; |
|||
} |
|||
$fields = [ |
|||
['V', ZipStream::DATA_DESCRIPTOR_SIGNATURE], |
|||
['V', $this->crc], // CRC32 |
|||
[$sizeFormat, $this->zlen], // Length of compressed data |
|||
[$sizeFormat, $this->len], // Length of original data |
|||
]; |
|||
|
|||
$footer = ZipStream::packFields($fields); |
|||
$this->zip->send($footer); |
|||
} else { |
|||
$footer = ''; |
|||
} |
|||
$this->totalLength = $this->hlen->add($this->zlen)->add(Bigint::init(strlen($footer))); |
|||
$this->zip->addToCdr($this); |
|||
} |
|||
|
|||
public function processStream(StreamInterface $stream): void |
|||
{ |
|||
$this->zlen = new Bigint(); |
|||
$this->len = new Bigint(); |
|||
|
|||
if ($this->zip->opt->isZeroHeader()) { |
|||
$this->processStreamWithZeroHeader($stream); |
|||
} else { |
|||
$this->processStreamWithComputedHeader($stream); |
|||
} |
|||
} |
|||
|
|||
protected function processStreamWithZeroHeader(StreamInterface $stream): void |
|||
{ |
|||
$this->bits |= self::BIT_ZERO_HEADER; |
|||
$this->addFileHeader(); |
|||
$this->readStream($stream, self::COMPUTE | self::SEND); |
|||
$this->addFileFooter(); |
|||
} |
|||
|
|||
protected function readStream(StreamInterface $stream, ?int $options = null): void |
|||
{ |
|||
$this->deflateInit(); |
|||
$total = 0; |
|||
$size = $this->opt->getSize(); |
|||
while (!$stream->eof() && ($size === 0 || $total < $size)) { |
|||
$data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE); |
|||
$total += strlen($data); |
|||
if ($size > 0 && $total > $size) { |
|||
$data = substr($data, 0 , strlen($data)-($total - $size)); |
|||
} |
|||
$this->deflateData($stream, $data, $options); |
|||
if ($options & self::SEND) { |
|||
$this->zip->send($data); |
|||
} |
|||
} |
|||
$this->deflateFinish($options); |
|||
} |
|||
|
|||
protected function deflateInit(): void |
|||
{ |
|||
$this->hash = hash_init(self::HASH_ALGORITHM); |
|||
if ($this->method->equals(Method::DEFLATE())) { |
|||
$this->deflate = deflate_init( |
|||
ZLIB_ENCODING_RAW, |
|||
['level' => $this->opt->getDeflateLevel()] |
|||
); |
|||
} |
|||
} |
|||
|
|||
protected function deflateData(StreamInterface $stream, string &$data, ?int $options = null): void |
|||
{ |
|||
if ($options & self::COMPUTE) { |
|||
$this->len = $this->len->add(Bigint::init(strlen($data))); |
|||
hash_update($this->hash, $data); |
|||
} |
|||
if ($this->deflate) { |
|||
$data = deflate_add( |
|||
$this->deflate, |
|||
$data, |
|||
$stream->eof() |
|||
? ZLIB_FINISH |
|||
: ZLIB_NO_FLUSH |
|||
); |
|||
} |
|||
if ($options & self::COMPUTE) { |
|||
$this->zlen = $this->zlen->add(Bigint::init(strlen($data))); |
|||
} |
|||
} |
|||
|
|||
protected function deflateFinish(?int $options = null): void |
|||
{ |
|||
if ($options & self::COMPUTE) { |
|||
$this->crc = hexdec(hash_final($this->hash)); |
|||
} |
|||
} |
|||
|
|||
protected function processStreamWithComputedHeader(StreamInterface $stream): void |
|||
{ |
|||
$this->readStream($stream, self::COMPUTE); |
|||
$stream->rewind(); |
|||
|
|||
// incremental compression with deflate_add |
|||
// makes this second read unnecessary |
|||
// but it is only available from PHP 7.0 |
|||
if (!$this->deflate && $stream instanceof DeflateStream && $this->method->equals(Method::DEFLATE())) { |
|||
$stream->addDeflateFilter($this->opt); |
|||
$this->zlen = new Bigint(); |
|||
while (!$stream->eof()) { |
|||
$data = $stream->read(self::CHUNKED_READ_BLOCK_SIZE); |
|||
$this->zlen = $this->zlen->add(Bigint::init(strlen($data))); |
|||
} |
|||
$stream->rewind(); |
|||
} |
|||
|
|||
$this->addFileHeader(); |
|||
$this->readStream($stream, self::SEND); |
|||
$this->addFileFooter(); |
|||
} |
|||
|
|||
/** |
|||
* Send CDR record for specified file. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getCdrFile(): string |
|||
{ |
|||
$name = static::filterFilename($this->name); |
|||
|
|||
// get attributes |
|||
$comment = $this->opt->getComment(); |
|||
|
|||
// get dos timestamp |
|||
$time = static::dosTime($this->opt->getTime()->getTimestamp()); |
|||
|
|||
$footer = $this->buildZip64ExtraBlock(); |
|||
|
|||
$fields = [ |
|||
['V', ZipStream::CDR_FILE_SIGNATURE], // Central file header signature |
|||
['v', ZipStream::ZIP_VERSION_MADE_BY], // Made by version |
|||
['v', $this->version->getValue()], // Extract by version |
|||
['v', $this->bits], // General purpose bit flags - data descriptor flag set |
|||
['v', $this->method->getValue()], // Compression method |
|||
['V', $time], // Timestamp (DOS Format) |
|||
['V', $this->crc], // CRC32 |
|||
['V', $this->zlen->getLowFF()], // Compressed Data Length |
|||
['V', $this->len->getLowFF()], // Original Data Length |
|||
['v', strlen($name)], // Length of filename |
|||
['v', strlen($footer)], // Extra data len (see above) |
|||
['v', strlen($comment)], // Length of comment |
|||
['v', 0], // Disk number |
|||
['v', 0], // Internal File Attributes |
|||
['V', 32], // External File Attributes |
|||
['V', $this->ofs->getLowFF()] // Relative offset of local header |
|||
]; |
|||
|
|||
// pack fields, then append name and comment |
|||
$header = ZipStream::packFields($fields); |
|||
|
|||
return $header . $name . $footer . $comment; |
|||
} |
|||
|
|||
/** |
|||
* @return Bigint |
|||
*/ |
|||
public function getTotalLength(): Bigint |
|||
{ |
|||
return $this->totalLength; |
|||
} |
|||
} |
|||
@ -0,0 +1,261 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Option; |
|||
|
|||
final class Archive |
|||
{ |
|||
const DEFAULT_DEFLATE_LEVEL = 6; |
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $comment = ''; |
|||
/** |
|||
* Size, in bytes, of the largest file to try |
|||
* and load into memory (used by |
|||
* addFileFromPath()). Large files may also |
|||
* be compressed differently; see the |
|||
* 'largeFileMethod' option. Default is ~20 Mb. |
|||
* |
|||
* @var int |
|||
*/ |
|||
private $largeFileSize = 20 * 1024 * 1024; |
|||
/** |
|||
* How to handle large files. Legal values are |
|||
* Method::STORE() (the default), or |
|||
* Method::DEFLATE(). STORE sends the file |
|||
* raw and is significantly |
|||
* faster, while DEFLATE compresses the file |
|||
* and is much, much slower. Note that DEFLATE |
|||
* must compress the file twice and is extremely slow. |
|||
* |
|||
* @var Method |
|||
*/ |
|||
private $largeFileMethod; |
|||
/** |
|||
* Boolean indicating whether or not to send |
|||
* the HTTP headers for this file. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
private $sendHttpHeaders = false; |
|||
/** |
|||
* The method called to send headers |
|||
* |
|||
* @var Callable |
|||
*/ |
|||
private $httpHeaderCallback = 'header'; |
|||
/** |
|||
* Enable Zip64 extension, supporting very large |
|||
* archives (any size > 4 GB or file count > 64k) |
|||
* |
|||
* @var bool |
|||
*/ |
|||
private $enableZip64 = true; |
|||
/** |
|||
* Enable streaming files with single read where |
|||
* general purpose bit 3 indicates local file header |
|||
* contain zero values in crc and size fields, |
|||
* these appear only after file contents |
|||
* in data descriptor block. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
private $zeroHeader = false; |
|||
/** |
|||
* Enable reading file stat for determining file size. |
|||
* When a 32-bit system reads file size that is |
|||
* over 2 GB, invalid value appears in file size |
|||
* due to integer overflow. Should be disabled on |
|||
* 32-bit systems with method addFileFromPath |
|||
* if any file may exceed 2 GB. In this case file |
|||
* will be read in blocks and correct size will be |
|||
* determined from content. |
|||
* |
|||
* @var bool |
|||
*/ |
|||
private $statFiles = true; |
|||
/** |
|||
* Enable flush after every write to output stream. |
|||
* @var bool |
|||
*/ |
|||
private $flushOutput = false; |
|||
/** |
|||
* HTTP Content-Disposition. Defaults to |
|||
* 'attachment', where |
|||
* FILENAME is the specified filename. |
|||
* |
|||
* Note that this does nothing if you are |
|||
* not sending HTTP headers. |
|||
* |
|||
* @var string |
|||
*/ |
|||
private $contentDisposition = 'attachment'; |
|||
/** |
|||
* Note that this does nothing if you are |
|||
* not sending HTTP headers. |
|||
* |
|||
* @var string |
|||
*/ |
|||
private $contentType = 'application/x-zip'; |
|||
/** |
|||
* @var int |
|||
*/ |
|||
private $deflateLevel = 6; |
|||
|
|||
/** |
|||
* @var resource |
|||
*/ |
|||
private $outputStream; |
|||
|
|||
/** |
|||
* Options constructor. |
|||
*/ |
|||
public function __construct() |
|||
{ |
|||
$this->largeFileMethod = Method::STORE(); |
|||
$this->outputStream = fopen('php://output', 'wb'); |
|||
} |
|||
|
|||
public function getComment(): string |
|||
{ |
|||
return $this->comment; |
|||
} |
|||
|
|||
public function setComment(string $comment): void |
|||
{ |
|||
$this->comment = $comment; |
|||
} |
|||
|
|||
public function getLargeFileSize(): int |
|||
{ |
|||
return $this->largeFileSize; |
|||
} |
|||
|
|||
public function setLargeFileSize(int $largeFileSize): void |
|||
{ |
|||
$this->largeFileSize = $largeFileSize; |
|||
} |
|||
|
|||
public function getLargeFileMethod(): Method |
|||
{ |
|||
return $this->largeFileMethod; |
|||
} |
|||
|
|||
public function setLargeFileMethod(Method $largeFileMethod): void |
|||
{ |
|||
$this->largeFileMethod = $largeFileMethod; |
|||
} |
|||
|
|||
public function isSendHttpHeaders(): bool |
|||
{ |
|||
return $this->sendHttpHeaders; |
|||
} |
|||
|
|||
public function setSendHttpHeaders(bool $sendHttpHeaders): void |
|||
{ |
|||
$this->sendHttpHeaders = $sendHttpHeaders; |
|||
} |
|||
|
|||
public function getHttpHeaderCallback(): Callable |
|||
{ |
|||
return $this->httpHeaderCallback; |
|||
} |
|||
|
|||
public function setHttpHeaderCallback(Callable $httpHeaderCallback): void |
|||
{ |
|||
$this->httpHeaderCallback = $httpHeaderCallback; |
|||
} |
|||
|
|||
public function isEnableZip64(): bool |
|||
{ |
|||
return $this->enableZip64; |
|||
} |
|||
|
|||
public function setEnableZip64(bool $enableZip64): void |
|||
{ |
|||
$this->enableZip64 = $enableZip64; |
|||
} |
|||
|
|||
public function isZeroHeader(): bool |
|||
{ |
|||
return $this->zeroHeader; |
|||
} |
|||
|
|||
public function setZeroHeader(bool $zeroHeader): void |
|||
{ |
|||
$this->zeroHeader = $zeroHeader; |
|||
} |
|||
|
|||
public function isFlushOutput(): bool |
|||
{ |
|||
return $this->flushOutput; |
|||
} |
|||
|
|||
public function setFlushOutput(bool $flushOutput): void |
|||
{ |
|||
$this->flushOutput = $flushOutput; |
|||
} |
|||
|
|||
public function isStatFiles(): bool |
|||
{ |
|||
return $this->statFiles; |
|||
} |
|||
|
|||
public function setStatFiles(bool $statFiles): void |
|||
{ |
|||
$this->statFiles = $statFiles; |
|||
} |
|||
|
|||
public function getContentDisposition(): string |
|||
{ |
|||
return $this->contentDisposition; |
|||
} |
|||
|
|||
public function setContentDisposition(string $contentDisposition): void |
|||
{ |
|||
$this->contentDisposition = $contentDisposition; |
|||
} |
|||
|
|||
public function getContentType(): string |
|||
{ |
|||
return $this->contentType; |
|||
} |
|||
|
|||
public function setContentType(string $contentType): void |
|||
{ |
|||
$this->contentType = $contentType; |
|||
} |
|||
|
|||
/** |
|||
* @return resource |
|||
*/ |
|||
public function getOutputStream() |
|||
{ |
|||
return $this->outputStream; |
|||
} |
|||
|
|||
/** |
|||
* @param resource $outputStream |
|||
*/ |
|||
public function setOutputStream($outputStream): void |
|||
{ |
|||
$this->outputStream = $outputStream; |
|||
} |
|||
|
|||
/** |
|||
* @return int |
|||
*/ |
|||
public function getDeflateLevel(): int |
|||
{ |
|||
return $this->deflateLevel; |
|||
} |
|||
|
|||
/** |
|||
* @param int $deflateLevel |
|||
*/ |
|||
public function setDeflateLevel(int $deflateLevel): void |
|||
{ |
|||
$this->deflateLevel = $deflateLevel; |
|||
} |
|||
} |
|||
@ -0,0 +1,116 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Option; |
|||
|
|||
use DateTime; |
|||
|
|||
final class File |
|||
{ |
|||
/** |
|||
* @var string |
|||
*/ |
|||
private $comment = ''; |
|||
/** |
|||
* @var Method |
|||
*/ |
|||
private $method; |
|||
/** |
|||
* @var int |
|||
*/ |
|||
private $deflateLevel; |
|||
/** |
|||
* @var DateTime |
|||
*/ |
|||
private $time; |
|||
/** |
|||
* @var int |
|||
*/ |
|||
private $size = 0; |
|||
|
|||
public function defaultTo(Archive $archiveOptions): void |
|||
{ |
|||
$this->deflateLevel = $this->deflateLevel ?: $archiveOptions->getDeflateLevel(); |
|||
$this->time = $this->time ?: new DateTime(); |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function getComment(): string |
|||
{ |
|||
return $this->comment; |
|||
} |
|||
|
|||
/** |
|||
* @param string $comment |
|||
*/ |
|||
public function setComment(string $comment): void |
|||
{ |
|||
$this->comment = $comment; |
|||
} |
|||
|
|||
/** |
|||
* @return Method |
|||
*/ |
|||
public function getMethod(): Method |
|||
{ |
|||
return $this->method ?: Method::DEFLATE(); |
|||
} |
|||
|
|||
/** |
|||
* @param Method $method |
|||
*/ |
|||
public function setMethod(Method $method): void |
|||
{ |
|||
$this->method = $method; |
|||
} |
|||
|
|||
/** |
|||
* @return int |
|||
*/ |
|||
public function getDeflateLevel(): int |
|||
{ |
|||
return $this->deflateLevel ?: Archive::DEFAULT_DEFLATE_LEVEL; |
|||
} |
|||
|
|||
/** |
|||
* @param int $deflateLevel |
|||
*/ |
|||
public function setDeflateLevel(int $deflateLevel): void |
|||
{ |
|||
$this->deflateLevel = $deflateLevel; |
|||
} |
|||
|
|||
/** |
|||
* @return DateTime |
|||
*/ |
|||
public function getTime(): DateTime |
|||
{ |
|||
return $this->time; |
|||
} |
|||
|
|||
/** |
|||
* @param DateTime $time |
|||
*/ |
|||
public function setTime(DateTime $time): void |
|||
{ |
|||
$this->time = $time; |
|||
} |
|||
|
|||
/** |
|||
* @return int |
|||
*/ |
|||
public function getSize(): int |
|||
{ |
|||
return $this->size; |
|||
} |
|||
|
|||
/** |
|||
* @param int $size |
|||
*/ |
|||
public function setSize(int $size): void |
|||
{ |
|||
$this->size = $size; |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Option; |
|||
|
|||
use MyCLabs\Enum\Enum; |
|||
|
|||
/** |
|||
* Methods enum |
|||
* |
|||
* @method static STORE(): Method |
|||
* @method static DEFLATE(): Method |
|||
* @psalm-immutable |
|||
*/ |
|||
class Method extends Enum |
|||
{ |
|||
const STORE = 0x00; |
|||
const DEFLATE = 0x08; |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream\Option; |
|||
|
|||
use MyCLabs\Enum\Enum; |
|||
|
|||
/** |
|||
* Class Version |
|||
* @package ZipStream\Option |
|||
* |
|||
* @method static STORE(): Version |
|||
* @method static DEFLATE(): Version |
|||
* @method static ZIP64(): Version |
|||
* @psalm-immutable |
|||
*/ |
|||
class Version extends Enum |
|||
{ |
|||
const STORE = 0x000A; // 1.00 |
|||
const DEFLATE = 0x0014; // 2.00 |
|||
const ZIP64 = 0x002D; // 4.50 |
|||
} |
|||
@ -0,0 +1,253 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream; |
|||
|
|||
use Psr\Http\Message\StreamInterface; |
|||
use RuntimeException; |
|||
|
|||
/** |
|||
* Describes a data stream. |
|||
* |
|||
* Typically, an instance will wrap a PHP stream; this interface provides |
|||
* a wrapper around the most common operations, including serialization of |
|||
* the entire stream to a string. |
|||
*/ |
|||
class Stream implements StreamInterface |
|||
{ |
|||
protected $stream; |
|||
|
|||
public function __construct($stream) |
|||
{ |
|||
$this->stream = $stream; |
|||
} |
|||
|
|||
/** |
|||
* Closes the stream and any underlying resources. |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function close(): void |
|||
{ |
|||
if (is_resource($this->stream)) { |
|||
fclose($this->stream); |
|||
} |
|||
$this->detach(); |
|||
} |
|||
|
|||
/** |
|||
* Separates any underlying resources from the stream. |
|||
* |
|||
* After the stream has been detached, the stream is in an unusable state. |
|||
* |
|||
* @return resource|null Underlying PHP stream, if any |
|||
*/ |
|||
public function detach() |
|||
{ |
|||
$result = $this->stream; |
|||
$this->stream = null; |
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Reads all data from the stream into a string, from the beginning to end. |
|||
* |
|||
* This method MUST attempt to seek to the beginning of the stream before |
|||
* reading data and read the stream until the end is reached. |
|||
* |
|||
* Warning: This could attempt to load a large amount of data into memory. |
|||
* |
|||
* This method MUST NOT raise an exception in order to conform with PHP's |
|||
* string casting operations. |
|||
* |
|||
* @see http://php.net/manual/en/language.oop5.magic.php#object.tostring |
|||
* @return string |
|||
*/ |
|||
public function __toString(): string |
|||
{ |
|||
try { |
|||
$this->seek(0); |
|||
} catch (\RuntimeException $e) {} |
|||
return (string) stream_get_contents($this->stream); |
|||
} |
|||
|
|||
/** |
|||
* Seek to a position in the stream. |
|||
* |
|||
* @link http://www.php.net/manual/en/function.fseek.php |
|||
* @param int $offset Stream offset |
|||
* @param int $whence Specifies how the cursor position will be calculated |
|||
* based on the seek offset. Valid values are identical to the built-in |
|||
* PHP $whence values for `fseek()`. SEEK_SET: Set position equal to |
|||
* offset bytes SEEK_CUR: Set position to current location plus offset |
|||
* SEEK_END: Set position to end-of-stream plus offset. |
|||
* @throws \RuntimeException on failure. |
|||
*/ |
|||
public function seek($offset, $whence = SEEK_SET): void |
|||
{ |
|||
if (!$this->isSeekable()) { |
|||
throw new RuntimeException; |
|||
} |
|||
if (fseek($this->stream, $offset, $whence) !== 0) { |
|||
throw new RuntimeException; |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns whether or not the stream is seekable. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isSeekable(): bool |
|||
{ |
|||
return (bool)$this->getMetadata('seekable'); |
|||
} |
|||
|
|||
/** |
|||
* Get stream metadata as an associative array or retrieve a specific key. |
|||
* |
|||
* The keys returned are identical to the keys returned from PHP's |
|||
* stream_get_meta_data() function. |
|||
* |
|||
* @link http://php.net/manual/en/function.stream-get-meta-data.php |
|||
* @param string $key Specific metadata to retrieve. |
|||
* @return array|mixed|null Returns an associative array if no key is |
|||
* provided. Returns a specific key value if a key is provided and the |
|||
* value is found, or null if the key is not found. |
|||
*/ |
|||
public function getMetadata($key = null) |
|||
{ |
|||
$metadata = stream_get_meta_data($this->stream); |
|||
return $key !== null ? @$metadata[$key] : $metadata; |
|||
} |
|||
|
|||
/** |
|||
* Get the size of the stream if known. |
|||
* |
|||
* @return int|null Returns the size in bytes if known, or null if unknown. |
|||
*/ |
|||
public function getSize(): ?int |
|||
{ |
|||
$stats = fstat($this->stream); |
|||
return $stats['size']; |
|||
} |
|||
|
|||
/** |
|||
* Returns the current position of the file read/write pointer |
|||
* |
|||
* @return int Position of the file pointer |
|||
* @throws \RuntimeException on error. |
|||
*/ |
|||
public function tell(): int |
|||
{ |
|||
$position = ftell($this->stream); |
|||
if ($position === false) { |
|||
throw new RuntimeException; |
|||
} |
|||
return $position; |
|||
} |
|||
|
|||
/** |
|||
* Returns true if the stream is at the end of the stream. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function eof(): bool |
|||
{ |
|||
return feof($this->stream); |
|||
} |
|||
|
|||
/** |
|||
* Seek to the beginning of the stream. |
|||
* |
|||
* If the stream is not seekable, this method will raise an exception; |
|||
* otherwise, it will perform a seek(0). |
|||
* |
|||
* @see seek() |
|||
* @link http://www.php.net/manual/en/function.fseek.php |
|||
* @throws \RuntimeException on failure. |
|||
*/ |
|||
public function rewind(): void |
|||
{ |
|||
$this->seek(0); |
|||
} |
|||
|
|||
/** |
|||
* Write data to the stream. |
|||
* |
|||
* @param string $string The string that is to be written. |
|||
* @return int Returns the number of bytes written to the stream. |
|||
* @throws \RuntimeException on failure. |
|||
*/ |
|||
public function write($string): int |
|||
{ |
|||
if (!$this->isWritable()) { |
|||
throw new RuntimeException; |
|||
} |
|||
if (fwrite($this->stream, $string) === false) { |
|||
throw new RuntimeException; |
|||
} |
|||
return \mb_strlen($string); |
|||
} |
|||
|
|||
/** |
|||
* Returns whether or not the stream is writable. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isWritable(): bool |
|||
{ |
|||
return preg_match('/[waxc+]/', $this->getMetadata('mode')) === 1; |
|||
} |
|||
|
|||
/** |
|||
* Read data from the stream. |
|||
* |
|||
* @param int $length Read up to $length bytes from the object and return |
|||
* them. Fewer than $length bytes may be returned if underlying stream |
|||
* call returns fewer bytes. |
|||
* @return string Returns the data read from the stream, or an empty string |
|||
* if no bytes are available. |
|||
* @throws \RuntimeException if an error occurs. |
|||
*/ |
|||
public function read($length): string |
|||
{ |
|||
if (!$this->isReadable()) { |
|||
throw new RuntimeException; |
|||
} |
|||
$result = fread($this->stream, $length); |
|||
if ($result === false) { |
|||
throw new RuntimeException; |
|||
} |
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Returns whether or not the stream is readable. |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isReadable(): bool |
|||
{ |
|||
return preg_match('/[r+]/', $this->getMetadata('mode')) === 1; |
|||
} |
|||
|
|||
/** |
|||
* Returns the remaining contents in a string |
|||
* |
|||
* @return string |
|||
* @throws \RuntimeException if unable to read or an error occurs while |
|||
* reading. |
|||
*/ |
|||
public function getContents(): string |
|||
{ |
|||
if (!$this->isReadable()) { |
|||
throw new RuntimeException; |
|||
} |
|||
$result = stream_get_contents($this->stream); |
|||
if ($result === false) { |
|||
throw new RuntimeException; |
|||
} |
|||
return $result; |
|||
} |
|||
} |
|||
@ -0,0 +1,599 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStream; |
|||
|
|||
use Psr\Http\Message\StreamInterface; |
|||
use ZipStream\Exception\OverflowException; |
|||
use ZipStream\Option\Archive as ArchiveOptions; |
|||
use ZipStream\Option\File as FileOptions; |
|||
use ZipStream\Option\Version; |
|||
|
|||
/** |
|||
* ZipStream |
|||
* |
|||
* Streamed, dynamically generated zip archives. |
|||
* |
|||
* Usage: |
|||
* |
|||
* Streaming zip archives is a simple, three-step process: |
|||
* |
|||
* 1. Create the zip stream: |
|||
* |
|||
* $zip = new ZipStream('example.zip'); |
|||
* |
|||
* 2. Add one or more files to the archive: |
|||
* |
|||
* * add first file |
|||
* $data = file_get_contents('some_file.gif'); |
|||
* $zip->addFile('some_file.gif', $data); |
|||
* |
|||
* * add second file |
|||
* $data = file_get_contents('some_file.gif'); |
|||
* $zip->addFile('another_file.png', $data); |
|||
* |
|||
* 3. Finish the zip stream: |
|||
* |
|||
* $zip->finish(); |
|||
* |
|||
* You can also add an archive comment, add comments to individual files, |
|||
* and adjust the timestamp of files. See the API documentation for each |
|||
* method below for additional information. |
|||
* |
|||
* Example: |
|||
* |
|||
* // create a new zip stream object |
|||
* $zip = new ZipStream('some_files.zip'); |
|||
* |
|||
* // list of local files |
|||
* $files = array('foo.txt', 'bar.jpg'); |
|||
* |
|||
* // read and add each file to the archive |
|||
* foreach ($files as $path) |
|||
* $zip->addFile($path, file_get_contents($path)); |
|||
* |
|||
* // write archive footer to stream |
|||
* $zip->finish(); |
|||
*/ |
|||
class ZipStream |
|||
{ |
|||
/** |
|||
* This number corresponds to the ZIP version/OS used (2 bytes) |
|||
* From: https://www.iana.org/assignments/media-types/application/zip |
|||
* The upper byte (leftmost one) indicates the host system (OS) for the |
|||
* file. Software can use this information to determine |
|||
* the line record format for text files etc. The current |
|||
* mappings are: |
|||
* |
|||
* 0 - MS-DOS and OS/2 (F.A.T. file systems) |
|||
* 1 - Amiga 2 - VAX/VMS |
|||
* 3 - *nix 4 - VM/CMS |
|||
* 5 - Atari ST 6 - OS/2 H.P.F.S. |
|||
* 7 - Macintosh 8 - Z-System |
|||
* 9 - CP/M 10 thru 255 - unused |
|||
* |
|||
* The lower byte (rightmost one) indicates the version number of the |
|||
* software used to encode the file. The value/10 |
|||
* indicates the major version number, and the value |
|||
* mod 10 is the minor version number. |
|||
* Here we are using 6 for the OS, indicating OS/2 H.P.F.S. |
|||
* to prevent file permissions issues upon extract (see #84) |
|||
* 0x603 is 00000110 00000011 in binary, so 6 and 3 |
|||
*/ |
|||
const ZIP_VERSION_MADE_BY = 0x603; |
|||
|
|||
/** |
|||
* The following signatures end with 0x4b50, which in ASCII is PK, |
|||
* the initials of the inventor Phil Katz. |
|||
* See https://en.wikipedia.org/wiki/Zip_(file_format)#File_headers |
|||
*/ |
|||
const FILE_HEADER_SIGNATURE = 0x04034b50; |
|||
const CDR_FILE_SIGNATURE = 0x02014b50; |
|||
const CDR_EOF_SIGNATURE = 0x06054b50; |
|||
const DATA_DESCRIPTOR_SIGNATURE = 0x08074b50; |
|||
const ZIP64_CDR_EOF_SIGNATURE = 0x06064b50; |
|||
const ZIP64_CDR_LOCATOR_SIGNATURE = 0x07064b50; |
|||
|
|||
/** |
|||
* Global Options |
|||
* |
|||
* @var ArchiveOptions |
|||
*/ |
|||
public $opt; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
public $files = []; |
|||
|
|||
/** |
|||
* @var Bigint |
|||
*/ |
|||
public $cdr_ofs; |
|||
|
|||
/** |
|||
* @var Bigint |
|||
*/ |
|||
public $ofs; |
|||
|
|||
/** |
|||
* @var bool |
|||
*/ |
|||
protected $need_headers; |
|||
|
|||
/** |
|||
* @var null|String |
|||
*/ |
|||
protected $output_name; |
|||
|
|||
/** |
|||
* Create a new ZipStream object. |
|||
* |
|||
* Parameters: |
|||
* |
|||
* @param String $name - Name of output file (optional). |
|||
* @param ArchiveOptions $opt - Archive Options |
|||
* |
|||
* Large File Support: |
|||
* |
|||
* By default, the method addFileFromPath() will send send files |
|||
* larger than 20 megabytes along raw rather than attempting to |
|||
* compress them. You can change both the maximum size and the |
|||
* compression behavior using the largeFile* options above, with the |
|||
* following caveats: |
|||
* |
|||
* * For "small" files (e.g. files smaller than largeFileSize), the |
|||
* memory use can be up to twice that of the actual file. In other |
|||
* words, adding a 10 megabyte file to the archive could potentially |
|||
* occupy 20 megabytes of memory. |
|||
* |
|||
* * Enabling compression on large files (e.g. files larger than |
|||
* large_file_size) is extremely slow, because ZipStream has to pass |
|||
* over the large file once to calculate header information, and then |
|||
* again to compress and send the actual data. |
|||
* |
|||
* Examples: |
|||
* |
|||
* // create a new zip file named 'foo.zip' |
|||
* $zip = new ZipStream('foo.zip'); |
|||
* |
|||
* // create a new zip file named 'bar.zip' with a comment |
|||
* $opt->setComment = 'this is a comment for the zip file.'; |
|||
* $zip = new ZipStream('bar.zip', $opt); |
|||
* |
|||
* Notes: |
|||
* |
|||
* In order to let this library send HTTP headers, a filename must be given |
|||
* _and_ the option `sendHttpHeaders` must be `true`. This behavior is to |
|||
* allow software to send its own headers (including the filename), and |
|||
* still use this library. |
|||
*/ |
|||
public function __construct(?string $name = null, ?ArchiveOptions $opt = null) |
|||
{ |
|||
$this->opt = $opt ?: new ArchiveOptions(); |
|||
|
|||
$this->output_name = $name; |
|||
$this->need_headers = $name && $this->opt->isSendHttpHeaders(); |
|||
|
|||
$this->cdr_ofs = new Bigint(); |
|||
$this->ofs = new Bigint(); |
|||
} |
|||
|
|||
/** |
|||
* addFile |
|||
* |
|||
* Add a file to the archive. |
|||
* |
|||
* @param String $name - path of file in archive (including directory). |
|||
* @param String $data - contents of file |
|||
* @param FileOptions $options |
|||
* |
|||
* File Options: |
|||
* time - Last-modified timestamp (seconds since the epoch) of |
|||
* this file. Defaults to the current time. |
|||
* comment - Comment related to this file. |
|||
* method - Storage method for file ("store" or "deflate") |
|||
* |
|||
* Examples: |
|||
* |
|||
* // add a file named 'foo.txt' |
|||
* $data = file_get_contents('foo.txt'); |
|||
* $zip->addFile('foo.txt', $data); |
|||
* |
|||
* // add a file named 'bar.jpg' with a comment and a last-modified |
|||
* // time of two hours ago |
|||
* $data = file_get_contents('bar.jpg'); |
|||
* $opt->setTime = time() - 2 * 3600; |
|||
* $opt->setComment = 'this is a comment about bar.jpg'; |
|||
* $zip->addFile('bar.jpg', $data, $opt); |
|||
*/ |
|||
public function addFile(string $name, string $data, ?FileOptions $options = null): void |
|||
{ |
|||
$options = $options ?: new FileOptions(); |
|||
$options->defaultTo($this->opt); |
|||
|
|||
$file = new File($this, $name, $options); |
|||
$file->processData($data); |
|||
} |
|||
|
|||
/** |
|||
* addFileFromPath |
|||
* |
|||
* Add a file at path to the archive. |
|||
* |
|||
* Note that large files may be compressed differently than smaller |
|||
* files; see the "Large File Support" section above for more |
|||
* information. |
|||
* |
|||
* @param String $name - name of file in archive (including directory path). |
|||
* @param String $path - path to file on disk (note: paths should be encoded using |
|||
* UNIX-style forward slashes -- e.g '/path/to/some/file'). |
|||
* @param FileOptions $options |
|||
* |
|||
* File Options: |
|||
* time - Last-modified timestamp (seconds since the epoch) of |
|||
* this file. Defaults to the current time. |
|||
* comment - Comment related to this file. |
|||
* method - Storage method for file ("store" or "deflate") |
|||
* |
|||
* Examples: |
|||
* |
|||
* // add a file named 'foo.txt' from the local file '/tmp/foo.txt' |
|||
* $zip->addFileFromPath('foo.txt', '/tmp/foo.txt'); |
|||
* |
|||
* // add a file named 'bigfile.rar' from the local file |
|||
* // '/usr/share/bigfile.rar' with a comment and a last-modified |
|||
* // time of two hours ago |
|||
* $path = '/usr/share/bigfile.rar'; |
|||
* $opt->setTime = time() - 2 * 3600; |
|||
* $opt->setComment = 'this is a comment about bar.jpg'; |
|||
* $zip->addFileFromPath('bigfile.rar', $path, $opt); |
|||
* |
|||
* @return void |
|||
* @throws \ZipStream\Exception\FileNotFoundException |
|||
* @throws \ZipStream\Exception\FileNotReadableException |
|||
*/ |
|||
public function addFileFromPath(string $name, string $path, ?FileOptions $options = null): void |
|||
{ |
|||
$options = $options ?: new FileOptions(); |
|||
$options->defaultTo($this->opt); |
|||
|
|||
$file = new File($this, $name, $options); |
|||
$file->processPath($path); |
|||
} |
|||
|
|||
/** |
|||
* addFileFromStream |
|||
* |
|||
* Add an open stream to the archive. |
|||
* |
|||
* @param String $name - path of file in archive (including directory). |
|||
* @param resource $stream - contents of file as a stream resource |
|||
* @param FileOptions $options |
|||
* |
|||
* File Options: |
|||
* time - Last-modified timestamp (seconds since the epoch) of |
|||
* this file. Defaults to the current time. |
|||
* comment - Comment related to this file. |
|||
* |
|||
* Examples: |
|||
* |
|||
* // create a temporary file stream and write text to it |
|||
* $fp = tmpfile(); |
|||
* fwrite($fp, 'The quick brown fox jumped over the lazy dog.'); |
|||
* |
|||
* // add a file named 'streamfile.txt' from the content of the stream |
|||
* $x->addFileFromStream('streamfile.txt', $fp); |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function addFileFromStream(string $name, $stream, ?FileOptions $options = null): void |
|||
{ |
|||
$options = $options ?: new FileOptions(); |
|||
$options->defaultTo($this->opt); |
|||
|
|||
$file = new File($this, $name, $options); |
|||
$file->processStream(new DeflateStream($stream)); |
|||
} |
|||
|
|||
/** |
|||
* addFileFromPsr7Stream |
|||
* |
|||
* Add an open stream to the archive. |
|||
* |
|||
* @param String $name - path of file in archive (including directory). |
|||
* @param StreamInterface $stream - contents of file as a stream resource |
|||
* @param FileOptions $options |
|||
* |
|||
* File Options: |
|||
* time - Last-modified timestamp (seconds since the epoch) of |
|||
* this file. Defaults to the current time. |
|||
* comment - Comment related to this file. |
|||
* |
|||
* Examples: |
|||
* |
|||
* // create a temporary file stream and write text to it |
|||
* $fp = tmpfile(); |
|||
* fwrite($fp, 'The quick brown fox jumped over the lazy dog.'); |
|||
* |
|||
* // add a file named 'streamfile.txt' from the content of the stream |
|||
* $x->addFileFromPsr7Stream('streamfile.txt', $fp); |
|||
* |
|||
* @return void |
|||
*/ |
|||
public function addFileFromPsr7Stream( |
|||
string $name, |
|||
StreamInterface $stream, |
|||
?FileOptions $options = null |
|||
): void { |
|||
$options = $options ?: new FileOptions(); |
|||
$options->defaultTo($this->opt); |
|||
|
|||
$file = new File($this, $name, $options); |
|||
$file->processStream($stream); |
|||
} |
|||
|
|||
/** |
|||
* finish |
|||
* |
|||
* Write zip footer to stream. |
|||
* |
|||
* Example: |
|||
* |
|||
* // add a list of files to the archive |
|||
* $files = array('foo.txt', 'bar.jpg'); |
|||
* foreach ($files as $path) |
|||
* $zip->addFile($path, file_get_contents($path)); |
|||
* |
|||
* // write footer to stream |
|||
* $zip->finish(); |
|||
* @return void |
|||
* |
|||
* @throws OverflowException |
|||
*/ |
|||
public function finish(): void |
|||
{ |
|||
// add trailing cdr file records |
|||
foreach ($this->files as $cdrFile) { |
|||
$this->send($cdrFile); |
|||
$this->cdr_ofs = $this->cdr_ofs->add(Bigint::init(strlen($cdrFile))); |
|||
} |
|||
|
|||
// Add 64bit headers (if applicable) |
|||
if (count($this->files) >= 0xFFFF || |
|||
$this->cdr_ofs->isOver32() || |
|||
$this->ofs->isOver32()) { |
|||
if (!$this->opt->isEnableZip64()) { |
|||
throw new OverflowException(); |
|||
} |
|||
|
|||
$this->addCdr64Eof(); |
|||
$this->addCdr64Locator(); |
|||
} |
|||
|
|||
// add trailing cdr eof record |
|||
$this->addCdrEof(); |
|||
|
|||
// The End |
|||
$this->clear(); |
|||
} |
|||
|
|||
/** |
|||
* Send ZIP64 CDR EOF (Central Directory Record End-of-File) record. |
|||
* |
|||
* @return void |
|||
*/ |
|||
protected function addCdr64Eof(): void |
|||
{ |
|||
$num_files = count($this->files); |
|||
$cdr_length = $this->cdr_ofs; |
|||
$cdr_offset = $this->ofs; |
|||
|
|||
$fields = [ |
|||
['V', static::ZIP64_CDR_EOF_SIGNATURE], // ZIP64 end of central file header signature |
|||
['P', 44], // Length of data below this header (length of block - 12) = 44 |
|||
['v', static::ZIP_VERSION_MADE_BY], // Made by version |
|||
['v', Version::ZIP64], // Extract by version |
|||
['V', 0x00], // disk number |
|||
['V', 0x00], // no of disks |
|||
['P', $num_files], // no of entries on disk |
|||
['P', $num_files], // no of entries in cdr |
|||
['P', $cdr_length], // CDR size |
|||
['P', $cdr_offset], // CDR offset |
|||
]; |
|||
|
|||
$ret = static::packFields($fields); |
|||
$this->send($ret); |
|||
} |
|||
|
|||
/** |
|||
* Create a format string and argument list for pack(), then call |
|||
* pack() and return the result. |
|||
* |
|||
* @param array $fields |
|||
* @return string |
|||
*/ |
|||
public static function packFields(array $fields): string |
|||
{ |
|||
$fmt = ''; |
|||
$args = []; |
|||
|
|||
// populate format string and argument list |
|||
foreach ($fields as [$format, $value]) { |
|||
if ($format === 'P') { |
|||
$fmt .= 'VV'; |
|||
if ($value instanceof Bigint) { |
|||
$args[] = $value->getLow32(); |
|||
$args[] = $value->getHigh32(); |
|||
} else { |
|||
$args[] = $value; |
|||
$args[] = 0; |
|||
} |
|||
} else { |
|||
if ($value instanceof Bigint) { |
|||
$value = $value->getLow32(); |
|||
} |
|||
$fmt .= $format; |
|||
$args[] = $value; |
|||
} |
|||
} |
|||
|
|||
// prepend format string to argument list |
|||
array_unshift($args, $fmt); |
|||
|
|||
// build output string from header and compressed data |
|||
return pack(...$args); |
|||
} |
|||
|
|||
/** |
|||
* Send string, sending HTTP headers if necessary. |
|||
* Flush output after write if configure option is set. |
|||
* |
|||
* @param String $str |
|||
* @return void |
|||
*/ |
|||
public function send(string $str): void |
|||
{ |
|||
if ($this->need_headers) { |
|||
$this->sendHttpHeaders(); |
|||
} |
|||
$this->need_headers = false; |
|||
|
|||
fwrite($this->opt->getOutputStream(), $str); |
|||
|
|||
if ($this->opt->isFlushOutput()) { |
|||
// flush output buffer if it is on and flushable |
|||
$status = ob_get_status(); |
|||
if (isset($status['flags']) && ($status['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE)) { |
|||
ob_flush(); |
|||
} |
|||
|
|||
// Flush system buffers after flushing userspace output buffer |
|||
flush(); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Send HTTP headers for this stream. |
|||
* |
|||
* @return void |
|||
*/ |
|||
protected function sendHttpHeaders(): void |
|||
{ |
|||
// grab content disposition |
|||
$disposition = $this->opt->getContentDisposition(); |
|||
|
|||
if ($this->output_name) { |
|||
// Various different browsers dislike various characters here. Strip them all for safety. |
|||
$safe_output = trim(str_replace(['"', "'", '\\', ';', "\n", "\r"], '', $this->output_name)); |
|||
|
|||
// Check if we need to UTF-8 encode the filename |
|||
$urlencoded = rawurlencode($safe_output); |
|||
$disposition .= "; filename*=UTF-8''{$urlencoded}"; |
|||
} |
|||
|
|||
$headers = array( |
|||
'Content-Type' => $this->opt->getContentType(), |
|||
'Content-Disposition' => $disposition, |
|||
'Pragma' => 'public', |
|||
'Cache-Control' => 'public, must-revalidate', |
|||
'Content-Transfer-Encoding' => 'binary' |
|||
); |
|||
|
|||
$call = $this->opt->getHttpHeaderCallback(); |
|||
foreach ($headers as $key => $val) { |
|||
$call("$key: $val"); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Send ZIP64 CDR Locator (Central Directory Record Locator) record. |
|||
* |
|||
* @return void |
|||
*/ |
|||
protected function addCdr64Locator(): void |
|||
{ |
|||
$cdr_offset = $this->ofs->add($this->cdr_ofs); |
|||
|
|||
$fields = [ |
|||
['V', static::ZIP64_CDR_LOCATOR_SIGNATURE], // ZIP64 end of central file header signature |
|||
['V', 0x00], // Disc number containing CDR64EOF |
|||
['P', $cdr_offset], // CDR offset |
|||
['V', 1], // Total number of disks |
|||
]; |
|||
|
|||
$ret = static::packFields($fields); |
|||
$this->send($ret); |
|||
} |
|||
|
|||
/** |
|||
* Send CDR EOF (Central Directory Record End-of-File) record. |
|||
* |
|||
* @return void |
|||
*/ |
|||
protected function addCdrEof(): void |
|||
{ |
|||
$num_files = count($this->files); |
|||
$cdr_length = $this->cdr_ofs; |
|||
$cdr_offset = $this->ofs; |
|||
|
|||
// grab comment (if specified) |
|||
$comment = $this->opt->getComment(); |
|||
|
|||
$fields = [ |
|||
['V', static::CDR_EOF_SIGNATURE], // end of central file header signature |
|||
['v', 0x00], // disk number |
|||
['v', 0x00], // no of disks |
|||
['v', min($num_files, 0xFFFF)], // no of entries on disk |
|||
['v', min($num_files, 0xFFFF)], // no of entries in cdr |
|||
['V', $cdr_length->getLowFF()], // CDR size |
|||
['V', $cdr_offset->getLowFF()], // CDR offset |
|||
['v', strlen($comment)], // Zip Comment size |
|||
]; |
|||
|
|||
$ret = static::packFields($fields) . $comment; |
|||
$this->send($ret); |
|||
} |
|||
|
|||
/** |
|||
* Clear all internal variables. Note that the stream object is not |
|||
* usable after this. |
|||
* |
|||
* @return void |
|||
*/ |
|||
protected function clear(): void |
|||
{ |
|||
$this->files = []; |
|||
$this->ofs = new Bigint(); |
|||
$this->cdr_ofs = new Bigint(); |
|||
$this->opt = new ArchiveOptions(); |
|||
} |
|||
|
|||
/** |
|||
* Is this file larger than large_file_size? |
|||
* |
|||
* @param string $path |
|||
* @return bool |
|||
*/ |
|||
public function isLargeFile(string $path): bool |
|||
{ |
|||
if (!$this->opt->isStatFiles()) { |
|||
return false; |
|||
} |
|||
$stat = stat($path); |
|||
return $stat['size'] > $this->opt->getLargeFileSize(); |
|||
} |
|||
|
|||
/** |
|||
* Save file attributes for trailing CDR record. |
|||
* |
|||
* @param File $file |
|||
* @return void |
|||
*/ |
|||
public function addToCdr(File $file): void |
|||
{ |
|||
$file->ofs = $this->ofs; |
|||
$this->ofs = $this->ofs->add($file->getTotalLength()); |
|||
$this->files[] = $file->getCdrFile(); |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace BigintTest; |
|||
|
|||
use OverflowException; |
|||
use PHPUnit\Framework\TestCase; |
|||
use ZipStream\Bigint; |
|||
|
|||
class BigintTest extends TestCase |
|||
{ |
|||
public function testConstruct(): void |
|||
{ |
|||
$bigint = new Bigint(0x12345678); |
|||
$this->assertSame('0x0000000012345678', $bigint->getHex64()); |
|||
$this->assertSame(0x12345678, $bigint->getLow32()); |
|||
$this->assertSame(0, $bigint->getHigh32()); |
|||
} |
|||
|
|||
public function testConstructLarge(): void |
|||
{ |
|||
$bigint = new Bigint(0x87654321); |
|||
$this->assertSame('0x0000000087654321', $bigint->getHex64()); |
|||
$this->assertSame('87654321', bin2hex(pack('N', $bigint->getLow32()))); |
|||
$this->assertSame(0, $bigint->getHigh32()); |
|||
} |
|||
|
|||
public function testAddSmallValue(): void |
|||
{ |
|||
$bigint = new Bigint(1); |
|||
$bigint = $bigint->add(Bigint::init(2)); |
|||
$this->assertSame(3, $bigint->getLow32()); |
|||
$this->assertFalse($bigint->isOver32()); |
|||
$this->assertTrue($bigint->isOver32(true)); |
|||
$this->assertSame($bigint->getLowFF(), (float)$bigint->getLow32()); |
|||
$this->assertSame($bigint->getLowFF(true), (float)0xFFFFFFFF); |
|||
} |
|||
|
|||
public function testAddWithOverflowAtLowestByte(): void |
|||
{ |
|||
$bigint = new Bigint(0xFF); |
|||
$bigint = $bigint->add(Bigint::init(0x01)); |
|||
$this->assertSame(0x100, $bigint->getLow32()); |
|||
} |
|||
|
|||
public function testAddWithOverflowAtInteger32(): void |
|||
{ |
|||
$bigint = new Bigint(0xFFFFFFFE); |
|||
$this->assertFalse($bigint->isOver32()); |
|||
$bigint = $bigint->add(Bigint::init(0x01)); |
|||
$this->assertTrue($bigint->isOver32()); |
|||
$bigint = $bigint->add(Bigint::init(0x01)); |
|||
$this->assertSame('0x0000000100000000', $bigint->getHex64()); |
|||
$this->assertTrue($bigint->isOver32()); |
|||
$this->assertSame((float)0xFFFFFFFF, $bigint->getLowFF()); |
|||
} |
|||
|
|||
public function testAddWithOverflowAtInteger64(): void |
|||
{ |
|||
$bigint = Bigint::fromLowHigh(0xFFFFFFFF, 0xFFFFFFFF); |
|||
$this->assertSame('0xFFFFFFFFFFFFFFFF', $bigint->getHex64()); |
|||
$this->expectException(OverflowException::class); |
|||
$bigint->add(Bigint::init(1)); |
|||
} |
|||
} |
|||
@ -0,0 +1,586 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace ZipStreamTest; |
|||
|
|||
use org\bovigo\vfs\vfsStream; |
|||
use GuzzleHttp\Psr7\Response; |
|||
use PHPUnit\Framework\TestCase; |
|||
use ZipStream\File; |
|||
use ZipStream\Option\Archive as ArchiveOptions; |
|||
use ZipStream\Option\File as FileOptions; |
|||
use ZipStream\Option\Method; |
|||
use ZipStream\ZipStream; |
|||
|
|||
/** |
|||
* Test Class for the Main ZipStream CLass |
|||
*/ |
|||
class ZipStreamTest extends TestCase |
|||
{ |
|||
const OSX_ARCHIVE_UTILITY = |
|||
'/System/Library/CoreServices/Applications/Archive Utility.app/Contents/MacOS/Archive Utility'; |
|||
|
|||
public function testFileNotFoundException(): void |
|||
{ |
|||
$this->expectException(\ZipStream\Exception\FileNotFoundException::class); |
|||
// Get ZipStream Object |
|||
$zip = new ZipStream(); |
|||
|
|||
// Trigger error by adding a file which doesn't exist |
|||
$zip->addFileFromPath('foobar.php', '/foo/bar/foobar.php'); |
|||
} |
|||
|
|||
public function testFileNotReadableException(): void |
|||
{ |
|||
// create new virtual filesystem |
|||
$root = vfsStream::setup('vfs'); |
|||
// create a virtual file with no permissions |
|||
$file = vfsStream::newFile('foo.txt', 0000)->at($root)->setContent('bar'); |
|||
$zip = new ZipStream(); |
|||
$this->expectException(\ZipStream\Exception\FileNotReadableException::class); |
|||
$zip->addFileFromPath('foo.txt', $file->url()); |
|||
} |
|||
|
|||
public function testDostime(): void |
|||
{ |
|||
// Allows testing of protected method |
|||
$class = new \ReflectionClass(File::class); |
|||
$method = $class->getMethod('dostime'); |
|||
$method->setAccessible(true); |
|||
|
|||
$this->assertSame($method->invoke(null, 1416246368), 1165069764); |
|||
|
|||
// January 1 1980 - DOS Epoch. |
|||
$this->assertSame($method->invoke(null, 315532800), 2162688); |
|||
|
|||
// January 1 1970 -> January 1 1980 due to minimum DOS Epoch. @todo Throw Exception? |
|||
$this->assertSame($method->invoke(null, 0), 2162688); |
|||
} |
|||
|
|||
public function testAddFile(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$zip->addFile('sample.txt', 'Sample String Data'); |
|||
$zip->addFile('test/sample.txt', 'More Simple Sample Data'); |
|||
|
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
|
|||
$files = $this->getRecursiveFileList($tmpDir); |
|||
$this->assertEquals(['sample.txt', 'test/sample.txt'], $files); |
|||
|
|||
$this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); |
|||
$this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
protected function getTmpFileStream(): array |
|||
{ |
|||
$tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest'); |
|||
$stream = fopen($tmp, 'wb+'); |
|||
|
|||
return array($tmp, $stream); |
|||
} |
|||
|
|||
/** |
|||
* @param string $tmp |
|||
* @return string |
|||
*/ |
|||
protected function validateAndExtractZip($tmp): string |
|||
{ |
|||
$tmpDir = $this->getTmpDir(); |
|||
|
|||
$zipArch = new \ZipArchive; |
|||
$res = $zipArch->open($tmp); |
|||
|
|||
if ($res !== true) { |
|||
$this->fail("Failed to open {$tmp}. Code: $res"); |
|||
|
|||
return $tmpDir; |
|||
} |
|||
|
|||
$this->assertEquals(0, $zipArch->status); |
|||
$this->assertEquals(0, $zipArch->statusSys); |
|||
|
|||
$zipArch->extractTo($tmpDir); |
|||
$zipArch->close(); |
|||
|
|||
return $tmpDir; |
|||
} |
|||
|
|||
protected function getTmpDir(): string |
|||
{ |
|||
$tmp = tempnam(sys_get_temp_dir(), 'zipstreamtest'); |
|||
unlink($tmp); |
|||
mkdir($tmp) or $this->fail('Failed to make directory'); |
|||
|
|||
return $tmp; |
|||
} |
|||
|
|||
/** |
|||
* @param string $path |
|||
* @return string[] |
|||
*/ |
|||
protected function getRecursiveFileList(string $path): array |
|||
{ |
|||
$data = array(); |
|||
$path = (string)realpath($path); |
|||
$files = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path)); |
|||
|
|||
$pathLen = strlen($path); |
|||
foreach ($files as $file) { |
|||
$filePath = $file->getRealPath(); |
|||
if (!is_dir($filePath)) { |
|||
$data[] = substr($filePath, $pathLen + 1); |
|||
} |
|||
} |
|||
|
|||
sort($data); |
|||
|
|||
return $data; |
|||
} |
|||
|
|||
public function testAddFileUtf8NameComment(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$name = 'árvíztűrő tükörfúrógép.txt'; |
|||
$content = 'Sample String Data'; |
|||
$comment = |
|||
'Filename has every special characters ' . |
|||
'from Hungarian language in lowercase. ' . |
|||
'In uppercase: ÁÍŰŐÜÖÚÓÉ'; |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setComment($comment); |
|||
|
|||
$zip->addFile($name, $content, $fileOptions); |
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
|
|||
$files = $this->getRecursiveFileList($tmpDir); |
|||
$this->assertEquals(array($name), $files); |
|||
$this->assertStringEqualsFile($tmpDir . '/' . $name, $content); |
|||
|
|||
$zipArch = new \ZipArchive(); |
|||
$zipArch->open($tmp); |
|||
$this->assertEquals($comment, $zipArch->getCommentName($name)); |
|||
} |
|||
|
|||
public function testAddFileUtf8NameNonUtfComment(): void |
|||
{ |
|||
$this->expectException(\ZipStream\Exception\EncodingException::class); |
|||
|
|||
$stream = $this->getTmpFileStream()[1]; |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$name = 'á.txt'; |
|||
$content = 'any'; |
|||
$comment = 'á'; |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setComment(mb_convert_encoding($comment, 'ISO-8859-2', 'UTF-8')); |
|||
|
|||
$zip->addFile($name, $content, $fileOptions); |
|||
} |
|||
|
|||
public function testAddFileNonUtf8NameUtfComment(): void |
|||
{ |
|||
$this->expectException(\ZipStream\Exception\EncodingException::class); |
|||
|
|||
$stream = $this->getTmpFileStream()[1]; |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$name = 'á.txt'; |
|||
$content = 'any'; |
|||
$comment = 'á'; |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setComment($comment); |
|||
|
|||
$zip->addFile(mb_convert_encoding($name, 'ISO-8859-2', 'UTF-8'), $content, $fileOptions); |
|||
} |
|||
|
|||
public function testAddFileWithStorageMethod(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setMethod(Method::STORE()); |
|||
|
|||
$zip->addFile('sample.txt', 'Sample String Data', $fileOptions); |
|||
$zip->addFile('test/sample.txt', 'More Simple Sample Data'); |
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$zipArch = new \ZipArchive(); |
|||
$zipArch->open($tmp); |
|||
|
|||
$sample1 = $zipArch->statName('sample.txt'); |
|||
$sample12 = $zipArch->statName('test/sample.txt'); |
|||
$this->assertEquals($sample1['comp_method'], Method::STORE); |
|||
$this->assertEquals($sample12['comp_method'], Method::DEFLATE); |
|||
|
|||
$zipArch->close(); |
|||
} |
|||
|
|||
public function testDecompressFileWithMacUnarchiver(): void |
|||
{ |
|||
if (!file_exists(self::OSX_ARCHIVE_UTILITY)) { |
|||
$this->markTestSkipped('The Mac OSX Archive Utility is not available.'); |
|||
} |
|||
|
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$folder = uniqid('', true); |
|||
|
|||
$zip->addFile($folder . '/sample.txt', 'Sample Data'); |
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
exec(escapeshellarg(self::OSX_ARCHIVE_UTILITY) . ' ' . escapeshellarg($tmp), $output, $returnStatus); |
|||
|
|||
$this->assertEquals(0, $returnStatus); |
|||
$this->assertCount(0, $output); |
|||
|
|||
$this->assertFileExists(dirname($tmp) . '/' . $folder . '/sample.txt'); |
|||
$this->assertStringEqualsFile(dirname($tmp) . '/' . $folder . '/sample.txt', 'Sample Data'); |
|||
} |
|||
|
|||
public function testAddFileFromPath(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
[$tmpExample, $streamExample] = $this->getTmpFileStream(); |
|||
fwrite($streamExample, 'Sample String Data'); |
|||
fclose($streamExample); |
|||
$zip->addFileFromPath('sample.txt', $tmpExample); |
|||
|
|||
[$tmpExample, $streamExample] = $this->getTmpFileStream(); |
|||
fwrite($streamExample, 'More Simple Sample Data'); |
|||
fclose($streamExample); |
|||
$zip->addFileFromPath('test/sample.txt', $tmpExample); |
|||
|
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
|
|||
$files = $this->getRecursiveFileList($tmpDir); |
|||
$this->assertEquals(array('sample.txt', 'test/sample.txt'), $files); |
|||
|
|||
$this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); |
|||
$this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); |
|||
} |
|||
|
|||
public function testAddFileFromPathWithStorageMethod(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setMethod(Method::STORE()); |
|||
|
|||
[$tmpExample, $streamExample] = $this->getTmpFileStream(); |
|||
fwrite($streamExample, 'Sample String Data'); |
|||
fclose($streamExample); |
|||
$zip->addFileFromPath('sample.txt', $tmpExample, $fileOptions); |
|||
|
|||
[$tmpExample, $streamExample] = $this->getTmpFileStream(); |
|||
fwrite($streamExample, 'More Simple Sample Data'); |
|||
fclose($streamExample); |
|||
$zip->addFileFromPath('test/sample.txt', $tmpExample); |
|||
|
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$zipArch = new \ZipArchive(); |
|||
$zipArch->open($tmp); |
|||
|
|||
$sample1 = $zipArch->statName('sample.txt'); |
|||
$this->assertEquals(Method::STORE, $sample1['comp_method']); |
|||
|
|||
$sample2 = $zipArch->statName('test/sample.txt'); |
|||
$this->assertEquals(Method::DEFLATE, $sample2['comp_method']); |
|||
|
|||
$zipArch->close(); |
|||
} |
|||
|
|||
public function testAddLargeFileFromPath(): void |
|||
{ |
|||
$methods = [Method::DEFLATE(), Method::STORE()]; |
|||
$falseTrue = [false, true]; |
|||
foreach ($methods as $method) { |
|||
foreach ($falseTrue as $zeroHeader) { |
|||
foreach ($falseTrue as $zip64) { |
|||
if ($zeroHeader && $method->equals(Method::DEFLATE())) { |
|||
continue; |
|||
} |
|||
$this->addLargeFileFileFromPath($method, $zeroHeader, $zip64); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
protected function addLargeFileFileFromPath($method, $zeroHeader, $zip64): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
$options->setLargeFileMethod($method); |
|||
$options->setLargeFileSize(5); |
|||
$options->setZeroHeader($zeroHeader); |
|||
$options->setEnableZip64($zip64); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
[$tmpExample, $streamExample] = $this->getTmpFileStream(); |
|||
for ($i = 0; $i <= 10000; $i++) { |
|||
fwrite($streamExample, sha1((string)$i)); |
|||
if ($i % 100 === 0) { |
|||
fwrite($streamExample, "\n"); |
|||
} |
|||
} |
|||
fclose($streamExample); |
|||
$shaExample = sha1_file($tmpExample); |
|||
$zip->addFileFromPath('sample.txt', $tmpExample); |
|||
unlink($tmpExample); |
|||
|
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
|
|||
$files = $this->getRecursiveFileList($tmpDir); |
|||
$this->assertEquals(array('sample.txt'), $files); |
|||
|
|||
$this->assertEquals(sha1_file($tmpDir . '/sample.txt'), $shaExample, "SHA-1 Mismatch Method: {$method}"); |
|||
} |
|||
|
|||
public function testAddFileFromStream(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
// In this test we can't use temporary stream to feed data |
|||
// because zlib.deflate filter gives empty string before PHP 7 |
|||
// it works fine with file stream |
|||
$streamExample = fopen(__FILE__, 'rb'); |
|||
$zip->addFileFromStream('sample.txt', $streamExample); |
|||
// fclose($streamExample); |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setMethod(Method::STORE()); |
|||
|
|||
$streamExample2 = fopen('php://temp', 'wb+'); |
|||
fwrite($streamExample2, 'More Simple Sample Data'); |
|||
rewind($streamExample2); // move the pointer back to the beginning of file. |
|||
$zip->addFileFromStream('test/sample.txt', $streamExample2, $fileOptions); |
|||
// fclose($streamExample2); |
|||
|
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
|
|||
$files = $this->getRecursiveFileList($tmpDir); |
|||
$this->assertEquals(array('sample.txt', 'test/sample.txt'), $files); |
|||
|
|||
$this->assertStringEqualsFile(__FILE__, file_get_contents($tmpDir . '/sample.txt')); |
|||
$this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); |
|||
} |
|||
|
|||
public function testAddFileFromStreamWithStorageMethod(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setMethod(Method::STORE()); |
|||
|
|||
$streamExample = fopen('php://temp', 'wb+'); |
|||
fwrite($streamExample, 'Sample String Data'); |
|||
rewind($streamExample); // move the pointer back to the beginning of file. |
|||
$zip->addFileFromStream('sample.txt', $streamExample, $fileOptions); |
|||
// fclose($streamExample); |
|||
|
|||
$streamExample2 = fopen('php://temp', 'bw+'); |
|||
fwrite($streamExample2, 'More Simple Sample Data'); |
|||
rewind($streamExample2); // move the pointer back to the beginning of file. |
|||
$zip->addFileFromStream('test/sample.txt', $streamExample2); |
|||
// fclose($streamExample2); |
|||
|
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$zipArch = new \ZipArchive(); |
|||
$zipArch->open($tmp); |
|||
|
|||
$sample1 = $zipArch->statName('sample.txt'); |
|||
$this->assertEquals(Method::STORE, $sample1['comp_method']); |
|||
|
|||
$sample2 = $zipArch->statName('test/sample.txt'); |
|||
$this->assertEquals(Method::DEFLATE, $sample2['comp_method']); |
|||
|
|||
$zipArch->close(); |
|||
} |
|||
|
|||
public function testAddFileFromPsr7Stream(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$body = 'Sample String Data'; |
|||
$response = new Response(200, [], $body); |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setMethod(Method::STORE()); |
|||
|
|||
$zip->addFileFromPsr7Stream('sample.json', $response->getBody(), $fileOptions); |
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
|
|||
$files = $this->getRecursiveFileList($tmpDir); |
|||
$this->assertEquals(array('sample.json'), $files); |
|||
$this->assertStringEqualsFile($tmpDir . '/sample.json', $body); |
|||
} |
|||
|
|||
public function testAddFileFromPsr7StreamWithFileSizeSet(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$body = 'Sample String Data'; |
|||
$fileSize = strlen($body); |
|||
// Add fake padding |
|||
$fakePadding = "\0\0\0\0\0\0"; |
|||
$response = new Response(200, [], $body . $fakePadding); |
|||
|
|||
$fileOptions = new FileOptions(); |
|||
$fileOptions->setMethod(Method::STORE()); |
|||
$fileOptions->setSize($fileSize); |
|||
$zip->addFileFromPsr7Stream('sample.json', $response->getBody(), $fileOptions); |
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
|
|||
$files = $this->getRecursiveFileList($tmpDir); |
|||
$this->assertEquals(array('sample.json'), $files); |
|||
$this->assertStringEqualsFile($tmpDir . '/sample.json', $body); |
|||
} |
|||
|
|||
public function testCreateArchiveWithFlushOptionSet(): void |
|||
{ |
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
$options->setFlushOutput(true); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$zip->addFile('sample.txt', 'Sample String Data'); |
|||
$zip->addFile('test/sample.txt', 'More Simple Sample Data'); |
|||
|
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
|
|||
$files = $this->getRecursiveFileList($tmpDir); |
|||
$this->assertEquals(['sample.txt', 'test/sample.txt'], $files); |
|||
|
|||
$this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); |
|||
$this->assertStringEqualsFile($tmpDir . '/test/sample.txt', 'More Simple Sample Data'); |
|||
} |
|||
|
|||
public function testCreateArchiveWithOutputBufferingOffAndFlushOptionSet(): void |
|||
{ |
|||
// WORKAROUND (1/2): remove phpunit's output buffer in order to run test without any buffering |
|||
ob_end_flush(); |
|||
$this->assertEquals(0, ob_get_level()); |
|||
|
|||
[$tmp, $stream] = $this->getTmpFileStream(); |
|||
|
|||
$options = new ArchiveOptions(); |
|||
$options->setOutputStream($stream); |
|||
$options->setFlushOutput(true); |
|||
|
|||
$zip = new ZipStream(null, $options); |
|||
|
|||
$zip->addFile('sample.txt', 'Sample String Data'); |
|||
|
|||
$zip->finish(); |
|||
fclose($stream); |
|||
|
|||
$tmpDir = $this->validateAndExtractZip($tmp); |
|||
$this->assertStringEqualsFile($tmpDir . '/sample.txt', 'Sample String Data'); |
|||
|
|||
// WORKAROUND (2/2): add back output buffering so that PHPUnit doesn't complain that it is missing |
|||
ob_start(); |
|||
} |
|||
} |
|||
@ -0,0 +1,6 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
date_default_timezone_set('UTC'); |
|||
|
|||
require __DIR__ . '/../vendor/autoload.php'; |
|||
@ -0,0 +1,39 @@ |
|||
<?php |
|||
declare(strict_types=1); |
|||
|
|||
namespace BugHonorFileTimeTest; |
|||
|
|||
use DateTime; |
|||
use PHPUnit\Framework\TestCase; |
|||
use ZipStream\Option\{ |
|||
Archive, |
|||
File |
|||
}; |
|||
use ZipStream\ZipStream; |
|||
|
|||
use function fopen; |
|||
|
|||
/** |
|||
* Asserts that specified last-modified timestamps are not overwritten when a |
|||
* file is added |
|||
*/ |
|||
class BugHonorFileTimeTest extends TestCase |
|||
{ |
|||
public function testHonorsFileTime(): void |
|||
{ |
|||
$archiveOpt = new Archive(); |
|||
$fileOpt = new File(); |
|||
$expectedTime = new DateTime('2019-04-21T19:25:00-0800'); |
|||
|
|||
$archiveOpt->setOutputStream(fopen('php://memory', 'wb')); |
|||
$fileOpt->setTime(clone $expectedTime); |
|||
|
|||
$zip = new ZipStream(null, $archiveOpt); |
|||
|
|||
$zip->addFile('sample.txt', 'Sample', $fileOpt); |
|||
|
|||
$zip->finish(); |
|||
|
|||
$this->assertEquals($expectedTime, $fileOpt->getTime()); |
|||
} |
|||
} |
|||
@ -0,0 +1,150 @@ |
|||
name: main |
|||
on: [ push, pull_request ] |
|||
jobs: |
|||
test: |
|||
runs-on: ubuntu-latest |
|||
strategy: |
|||
matrix: |
|||
experimental: |
|||
- false |
|||
php-version: |
|||
- '7.2' |
|||
- '7.3' |
|||
- '7.4' |
|||
- '8.0' |
|||
|
|||
include: |
|||
- php-version: '8.1' |
|||
experimental: true |
|||
|
|||
name: PHP ${{ matrix.php-version }} |
|||
|
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
- name: Setup PHP, with composer and extensions |
|||
uses: shivammathur/setup-php@v2 |
|||
with: |
|||
php-version: ${{ matrix.php-version }} |
|||
coverage: none |
|||
|
|||
- name: Get composer cache directory |
|||
id: composer-cache |
|||
run: echo "::set-output name=dir::$(composer config cache-files-dir)" |
|||
|
|||
- name: Cache composer dependencies |
|||
uses: actions/cache@v2 |
|||
with: |
|||
path: ${{ steps.composer-cache.outputs.dir }} |
|||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} |
|||
restore-keys: ${{ runner.os }}-composer- |
|||
|
|||
- name: Delete composer lock file |
|||
id: composer-lock |
|||
if: ${{ matrix.php-version == '8.1' }} |
|||
run: | |
|||
echo "::set-output name=flags::--ignore-platform-reqs" |
|||
|
|||
- name: Install dependencies |
|||
run: composer update --no-progress --prefer-dist --optimize-autoloader ${{ steps.composer-lock.outputs.flags }} |
|||
|
|||
- name: Setup problem matchers for PHP |
|||
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" |
|||
|
|||
- name: Setup problem matchers for PHPUnit |
|||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" |
|||
|
|||
- name: "Run PHPUnit tests (Experimental: ${{ matrix.experimental }})" |
|||
env: |
|||
FAILURE_ACTION: "${{ matrix.experimental == true }}" |
|||
run: vendor/bin/phpunit --verbose || $FAILURE_ACTION |
|||
|
|||
phpcs: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
- name: Setup PHP, with composer and extensions |
|||
uses: shivammathur/setup-php@v2 |
|||
with: |
|||
php-version: 7.4 |
|||
coverage: none |
|||
tools: cs2pr |
|||
|
|||
- name: Get composer cache directory |
|||
id: composer-cache |
|||
run: echo "::set-output name=dir::$(composer config cache-files-dir)" |
|||
|
|||
- name: Cache composer dependencies |
|||
uses: actions/cache@v2 |
|||
with: |
|||
path: ${{ steps.composer-cache.outputs.dir }} |
|||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} |
|||
restore-keys: ${{ runner.os }}-composer- |
|||
|
|||
- name: Install dependencies |
|||
run: composer install --no-progress --prefer-dist --optimize-autoloader |
|||
|
|||
- name: Code style with PHP_CodeSniffer |
|||
run: ./vendor/bin/phpcs -q --report=checkstyle classes/src/ | cs2pr |
|||
|
|||
versions: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
- name: Setup PHP, with composer and extensions |
|||
uses: shivammathur/setup-php@v2 |
|||
with: |
|||
php-version: 7.4 |
|||
coverage: none |
|||
tools: cs2pr |
|||
|
|||
- name: Get composer cache directory |
|||
id: composer-cache |
|||
run: echo "::set-output name=dir::$(composer config cache-files-dir)" |
|||
|
|||
- name: Cache composer dependencies |
|||
uses: actions/cache@v2 |
|||
with: |
|||
path: ${{ steps.composer-cache.outputs.dir }} |
|||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} |
|||
restore-keys: ${{ runner.os }}-composer- |
|||
|
|||
- name: Install dependencies |
|||
run: composer install --no-progress --prefer-dist --optimize-autoloader |
|||
|
|||
- name: Code Version Compatibility check with PHP_CodeSniffer |
|||
run: ./vendor/bin/phpcs -q --report-width=200 --report=summary,full classes/src/ --standard=PHPCompatibility --runtime-set testVersion 7.2- |
|||
|
|||
coverage: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
- name: Setup PHP, with composer and extensions |
|||
uses: shivammathur/setup-php@v2 |
|||
with: |
|||
php-version: 7.4 |
|||
coverage: pcov |
|||
|
|||
- name: Get composer cache directory |
|||
id: composer-cache |
|||
run: echo "::set-output name=dir::$(composer config cache-files-dir)" |
|||
|
|||
- name: Cache composer dependencies |
|||
uses: actions/cache@v2 |
|||
with: |
|||
path: ${{ steps.composer-cache.outputs.dir }} |
|||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} |
|||
restore-keys: ${{ runner.os }}-composer- |
|||
|
|||
- name: Install dependencies |
|||
run: composer install --no-progress --prefer-dist --optimize-autoloader |
|||
|
|||
- name: Test Coverage |
|||
run: ./vendor/bin/phpunit --verbose --coverage-text |
|||
@ -0,0 +1,173 @@ |
|||
PHPComplex |
|||
========== |
|||
|
|||
--- |
|||
|
|||
PHP Class Library for working with Complex numbers |
|||
|
|||
[](https://github.com/MarkBaker/PHPComplex/actions) |
|||
[](https://packagist.org/packages/markbaker/complex) |
|||
[](https://packagist.org/packages/markbaker/complex) |
|||
[](https://packagist.org/packages/markbaker/complex) |
|||
|
|||
|
|||
[](https://xkcd.com/2028/) |
|||
|
|||
--- |
|||
|
|||
The library currently provides the following operations: |
|||
|
|||
- addition |
|||
- subtraction |
|||
- multiplication |
|||
- division |
|||
- division by |
|||
- division into |
|||
|
|||
together with functions for |
|||
|
|||
- theta (polar theta angle) |
|||
- rho (polar distance/radius) |
|||
- conjugate |
|||
* negative |
|||
- inverse (1 / complex) |
|||
- cos (cosine) |
|||
- acos (inverse cosine) |
|||
- cosh (hyperbolic cosine) |
|||
- acosh (inverse hyperbolic cosine) |
|||
- sin (sine) |
|||
- asin (inverse sine) |
|||
- sinh (hyperbolic sine) |
|||
- asinh (inverse hyperbolic sine) |
|||
- sec (secant) |
|||
- asec (inverse secant) |
|||
- sech (hyperbolic secant) |
|||
- asech (inverse hyperbolic secant) |
|||
- csc (cosecant) |
|||
- acsc (inverse cosecant) |
|||
- csch (hyperbolic secant) |
|||
- acsch (inverse hyperbolic secant) |
|||
- tan (tangent) |
|||
- atan (inverse tangent) |
|||
- tanh (hyperbolic tangent) |
|||
- atanh (inverse hyperbolic tangent) |
|||
- cot (cotangent) |
|||
- acot (inverse cotangent) |
|||
- coth (hyperbolic cotangent) |
|||
- acoth (inverse hyperbolic cotangent) |
|||
- sqrt (square root) |
|||
- exp (exponential) |
|||
- ln (natural log) |
|||
- log10 (base-10 log) |
|||
- log2 (base-2 log) |
|||
- pow (raised to the power of a real number) |
|||
|
|||
|
|||
--- |
|||
|
|||
# Installation |
|||
|
|||
```shell |
|||
composer require markbaker/complex:^3.0 |
|||
``` |
|||
|
|||
# Important BC Note |
|||
|
|||
If you've previously been using procedural calls to functions and operations using this library, then from version 3.0 you should use [MarkBaker/PHPComplexFunctions](https://github.com/MarkBaker/PHPComplexFunctions) instead (available on packagist as [markbaker/complex-functions](https://packagist.org/packages/markbaker/complex-functions)). |
|||
|
|||
You'll need to replace `markbaker/complex`in your `composer.json` file with the new library, but otherwise there should be no difference in the namespacing, or in the way that you have called the Complex functions in the past, so no actual code changes are required. |
|||
|
|||
```shell |
|||
composer require markbaker/complex-functions:^1.0 |
|||
``` |
|||
|
|||
You should not reference this library (`markbaker/complex`) in your `composer.json`, composer wil take care of that for you. |
|||
|
|||
# Usage |
|||
|
|||
To create a new complex object, you can provide either the real, imaginary and suffix parts as individual values, or as an array of values passed passed to the constructor; or a string representing the value. e.g |
|||
|
|||
```php |
|||
$real = 1.23; |
|||
$imaginary = -4.56; |
|||
$suffix = 'i'; |
|||
|
|||
$complexObject = new Complex\Complex($real, $imaginary, $suffix); |
|||
``` |
|||
or as an array |
|||
```php |
|||
$real = 1.23; |
|||
$imaginary = -4.56; |
|||
$suffix = 'i'; |
|||
|
|||
$arguments = [$real, $imaginary, $suffix]; |
|||
|
|||
$complexObject = new Complex\Complex($arguments); |
|||
``` |
|||
or as a string |
|||
```php |
|||
$complexString = '1.23-4.56i'; |
|||
|
|||
$complexObject = new Complex\Complex($complexString); |
|||
``` |
|||
|
|||
Complex objects are immutable: whenever you call a method or pass a complex value to a function that returns a complex value, a new Complex object will be returned, and the original will remain unchanged. |
|||
This also allows you to chain multiple methods as you would for a fluent interface (as long as they are methods that will return a Complex result). |
|||
|
|||
## Performing Mathematical Operations |
|||
|
|||
To perform mathematical operations with Complex values, you can call the appropriate method against a complex value, passing other values as arguments |
|||
|
|||
```php |
|||
$complexString1 = '1.23-4.56i'; |
|||
$complexString2 = '2.34+5.67i'; |
|||
|
|||
$complexObject = new Complex\Complex($complexString1); |
|||
echo $complexObject->add($complexString2); |
|||
``` |
|||
|
|||
or use the static Operation methods |
|||
```php |
|||
$complexString1 = '1.23-4.56i'; |
|||
$complexString2 = '2.34+5.67i'; |
|||
|
|||
echo Complex\Operations::add($complexString1, $complexString2); |
|||
``` |
|||
If you want to perform the same operation against multiple values (e.g. to add three or more complex numbers), then you can pass multiple arguments to any of the operations. |
|||
|
|||
You can pass these arguments as Complex objects, or as an array, or string that will parse to a complex object. |
|||
|
|||
## Using functions |
|||
|
|||
When calling any of the available functions for a complex value, you can either call the relevant method for the Complex object |
|||
```php |
|||
$complexString = '1.23-4.56i'; |
|||
|
|||
$complexObject = new Complex\Complex($complexString); |
|||
echo $complexObject->sinh(); |
|||
``` |
|||
|
|||
or use the static Functions methods |
|||
```php |
|||
$complexString = '1.23-4.56i'; |
|||
|
|||
echo Complex\Functions::sinh($complexString); |
|||
``` |
|||
As with operations, you can pass these arguments as Complex objects, or as an array or string that will parse to a complex object. |
|||
|
|||
|
|||
In the case of the `pow()` function (the only implemented function that requires an additional argument) you need to pass both arguments when calling the function |
|||
|
|||
```php |
|||
$complexString = '1.23-4.56i'; |
|||
|
|||
$complexObject = new Complex\Complex($complexString); |
|||
echo Complex\Functions::pow($complexObject, 2); |
|||
``` |
|||
or pass the additional argument when calling the method |
|||
```php |
|||
$complexString = '1.23-4.56i'; |
|||
|
|||
$complexObject = new Complex\Complex($complexString); |
|||
echo $complexObject->pow(2); |
|||
``` |
|||
@ -0,0 +1,388 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* |
|||
* Class for the management of Complex numbers |
|||
* |
|||
* @copyright Copyright (c) 2013-2018 Mark Baker (https://github.com/MarkBaker/PHPComplex) |
|||
* @license https://opensource.org/licenses/MIT MIT |
|||
*/ |
|||
namespace Complex; |
|||
|
|||
/** |
|||
* Complex Number object. |
|||
* |
|||
* @package Complex |
|||
* |
|||
* @method float abs() |
|||
* @method Complex acos() |
|||
* @method Complex acosh() |
|||
* @method Complex acot() |
|||
* @method Complex acoth() |
|||
* @method Complex acsc() |
|||
* @method Complex acsch() |
|||
* @method float argument() |
|||
* @method Complex asec() |
|||
* @method Complex asech() |
|||
* @method Complex asin() |
|||
* @method Complex asinh() |
|||
* @method Complex atan() |
|||
* @method Complex atanh() |
|||
* @method Complex conjugate() |
|||
* @method Complex cos() |
|||
* @method Complex cosh() |
|||
* @method Complex cot() |
|||
* @method Complex coth() |
|||
* @method Complex csc() |
|||
* @method Complex csch() |
|||
* @method Complex exp() |
|||
* @method Complex inverse() |
|||
* @method Complex ln() |
|||
* @method Complex log2() |
|||
* @method Complex log10() |
|||
* @method Complex negative() |
|||
* @method Complex pow(int|float $power) |
|||
* @method float rho() |
|||
* @method Complex sec() |
|||
* @method Complex sech() |
|||
* @method Complex sin() |
|||
* @method Complex sinh() |
|||
* @method Complex sqrt() |
|||
* @method Complex tan() |
|||
* @method Complex tanh() |
|||
* @method float theta() |
|||
* @method Complex add(...$complexValues) |
|||
* @method Complex subtract(...$complexValues) |
|||
* @method Complex multiply(...$complexValues) |
|||
* @method Complex divideby(...$complexValues) |
|||
* @method Complex divideinto(...$complexValues) |
|||
*/ |
|||
class Complex |
|||
{ |
|||
/** |
|||
* @constant Euler's Number. |
|||
*/ |
|||
const EULER = 2.7182818284590452353602874713526624977572; |
|||
|
|||
/** |
|||
* @constant Regexp to split an input string into real and imaginary components and suffix |
|||
*/ |
|||
const NUMBER_SPLIT_REGEXP = |
|||
'` ^ |
|||
( # Real part |
|||
[-+]?(\d+\.?\d*|\d*\.?\d+) # Real value (integer or float) |
|||
([Ee][-+]?[0-2]?\d{1,3})? # Optional real exponent for scientific format |
|||
) |
|||
( # Imaginary part |
|||
[-+]?(\d+\.?\d*|\d*\.?\d+) # Imaginary value (integer or float) |
|||
([Ee][-+]?[0-2]?\d{1,3})? # Optional imaginary exponent for scientific format |
|||
)? |
|||
( # Imaginary part is optional |
|||
([-+]?) # Imaginary (implicit 1 or -1) only |
|||
([ij]?) # Imaginary i or j - depending on whether mathematical or engineering |
|||
) |
|||
$`uix'; |
|||
|
|||
/** |
|||
* @var float $realPart The value of of this complex number on the real plane. |
|||
*/ |
|||
protected $realPart = 0.0; |
|||
|
|||
/** |
|||
* @var float $imaginaryPart The value of of this complex number on the imaginary plane. |
|||
*/ |
|||
protected $imaginaryPart = 0.0; |
|||
|
|||
/** |
|||
* @var string $suffix The suffix for this complex number (i or j). |
|||
*/ |
|||
protected $suffix; |
|||
|
|||
|
|||
/** |
|||
* Validates whether the argument is a valid complex number, converting scalar or array values if possible |
|||
* |
|||
* @param mixed $complexNumber The value to parse |
|||
* @return array |
|||
* @throws Exception If the argument isn't a Complex number or cannot be converted to one |
|||
*/ |
|||
private static function parseComplex($complexNumber) |
|||
{ |
|||
// Test for real number, with no imaginary part |
|||
if (is_numeric($complexNumber)) { |
|||
return [$complexNumber, 0, null]; |
|||
} |
|||
|
|||
// Fix silly human errors |
|||
$complexNumber = str_replace( |
|||
['+-', '-+', '++', '--'], |
|||
['-', '-', '+', '+'], |
|||
$complexNumber |
|||
); |
|||
|
|||
// Basic validation of string, to parse out real and imaginary parts, and any suffix |
|||
$validComplex = preg_match( |
|||
self::NUMBER_SPLIT_REGEXP, |
|||
$complexNumber, |
|||
$complexParts |
|||
); |
|||
|
|||
if (!$validComplex) { |
|||
// Neither real nor imaginary part, so test to see if we actually have a suffix |
|||
$validComplex = preg_match('/^([\-\+]?)([ij])$/ui', $complexNumber, $complexParts); |
|||
if (!$validComplex) { |
|||
throw new Exception('Invalid complex number'); |
|||
} |
|||
// We have a suffix, so set the real to 0, the imaginary to either 1 or -1 (as defined by the sign) |
|||
$imaginary = 1; |
|||
if ($complexParts[1] === '-') { |
|||
$imaginary = 0 - $imaginary; |
|||
} |
|||
return [0, $imaginary, $complexParts[2]]; |
|||
} |
|||
|
|||
// If we don't have an imaginary part, identify whether it should be +1 or -1... |
|||
if (($complexParts[4] === '') && ($complexParts[9] !== '')) { |
|||
if ($complexParts[7] !== $complexParts[9]) { |
|||
$complexParts[4] = 1; |
|||
if ($complexParts[8] === '-') { |
|||
$complexParts[4] = -1; |
|||
} |
|||
} else { |
|||
// ... or if we have only the real and no imaginary part |
|||
// (in which case our real should be the imaginary) |
|||
$complexParts[4] = $complexParts[1]; |
|||
$complexParts[1] = 0; |
|||
} |
|||
} |
|||
|
|||
// Return real and imaginary parts and suffix as an array, and set a default suffix if user input lazily |
|||
return [ |
|||
$complexParts[1], |
|||
$complexParts[4], |
|||
!empty($complexParts[9]) ? $complexParts[9] : 'i' |
|||
]; |
|||
} |
|||
|
|||
|
|||
public function __construct($realPart = 0.0, $imaginaryPart = null, $suffix = 'i') |
|||
{ |
|||
if ($imaginaryPart === null) { |
|||
if (is_array($realPart)) { |
|||
// We have an array of (potentially) real and imaginary parts, and any suffix |
|||
list ($realPart, $imaginaryPart, $suffix) = array_values($realPart) + [0.0, 0.0, 'i']; |
|||
} elseif ((is_string($realPart)) || (is_numeric($realPart))) { |
|||
// We've been given a string to parse to extract the real and imaginary parts, and any suffix |
|||
list($realPart, $imaginaryPart, $suffix) = self::parseComplex($realPart); |
|||
} |
|||
} |
|||
|
|||
if ($imaginaryPart != 0.0 && empty($suffix)) { |
|||
$suffix = 'i'; |
|||
} elseif ($imaginaryPart == 0.0 && !empty($suffix)) { |
|||
$suffix = ''; |
|||
} |
|||
|
|||
// Set parsed values in our properties |
|||
$this->realPart = (float) $realPart; |
|||
$this->imaginaryPart = (float) $imaginaryPart; |
|||
$this->suffix = strtolower($suffix ?? ''); |
|||
} |
|||
|
|||
/** |
|||
* Gets the real part of this complex number |
|||
* |
|||
* @return Float |
|||
*/ |
|||
public function getReal(): float |
|||
{ |
|||
return $this->realPart; |
|||
} |
|||
|
|||
/** |
|||
* Gets the imaginary part of this complex number |
|||
* |
|||
* @return Float |
|||
*/ |
|||
public function getImaginary(): float |
|||
{ |
|||
return $this->imaginaryPart; |
|||
} |
|||
|
|||
/** |
|||
* Gets the suffix of this complex number |
|||
* |
|||
* @return String |
|||
*/ |
|||
public function getSuffix(): string |
|||
{ |
|||
return $this->suffix; |
|||
} |
|||
|
|||
/** |
|||
* Returns true if this is a real value, false if a complex value |
|||
* |
|||
* @return Bool |
|||
*/ |
|||
public function isReal(): bool |
|||
{ |
|||
return $this->imaginaryPart == 0.0; |
|||
} |
|||
|
|||
/** |
|||
* Returns true if this is a complex value, false if a real value |
|||
* |
|||
* @return Bool |
|||
*/ |
|||
public function isComplex(): bool |
|||
{ |
|||
return !$this->isReal(); |
|||
} |
|||
|
|||
public function format(): string |
|||
{ |
|||
$str = ""; |
|||
if ($this->imaginaryPart != 0.0) { |
|||
if (\abs($this->imaginaryPart) != 1.0) { |
|||
$str .= $this->imaginaryPart . $this->suffix; |
|||
} else { |
|||
$str .= (($this->imaginaryPart < 0.0) ? '-' : '') . $this->suffix; |
|||
} |
|||
} |
|||
if ($this->realPart != 0.0) { |
|||
if (($str) && ($this->imaginaryPart > 0.0)) { |
|||
$str = "+" . $str; |
|||
} |
|||
$str = $this->realPart . $str; |
|||
} |
|||
if (!$str) { |
|||
$str = "0.0"; |
|||
} |
|||
|
|||
return $str; |
|||
} |
|||
|
|||
public function __toString(): string |
|||
{ |
|||
return $this->format(); |
|||
} |
|||
|
|||
/** |
|||
* Validates whether the argument is a valid complex number, converting scalar or array values if possible |
|||
* |
|||
* @param mixed $complex The value to validate |
|||
* @return Complex |
|||
* @throws Exception If the argument isn't a Complex number or cannot be converted to one |
|||
*/ |
|||
public static function validateComplexArgument($complex): Complex |
|||
{ |
|||
if (is_scalar($complex) || is_array($complex)) { |
|||
$complex = new Complex($complex); |
|||
} elseif (!is_object($complex) || !($complex instanceof Complex)) { |
|||
throw new Exception('Value is not a valid complex number'); |
|||
} |
|||
|
|||
return $complex; |
|||
} |
|||
|
|||
/** |
|||
* Returns the reverse of this complex number |
|||
* |
|||
* @return Complex |
|||
*/ |
|||
public function reverse(): Complex |
|||
{ |
|||
return new Complex( |
|||
$this->imaginaryPart, |
|||
$this->realPart, |
|||
($this->realPart == 0.0) ? null : $this->suffix |
|||
); |
|||
} |
|||
|
|||
public function invertImaginary(): Complex |
|||
{ |
|||
return new Complex( |
|||
$this->realPart, |
|||
$this->imaginaryPart * -1, |
|||
($this->imaginaryPart == 0.0) ? null : $this->suffix |
|||
); |
|||
} |
|||
|
|||
public function invertReal(): Complex |
|||
{ |
|||
return new Complex( |
|||
$this->realPart * -1, |
|||
$this->imaginaryPart, |
|||
($this->imaginaryPart == 0.0) ? null : $this->suffix |
|||
); |
|||
} |
|||
|
|||
protected static $functions = [ |
|||
'abs', |
|||
'acos', |
|||
'acosh', |
|||
'acot', |
|||
'acoth', |
|||
'acsc', |
|||
'acsch', |
|||
'argument', |
|||
'asec', |
|||
'asech', |
|||
'asin', |
|||
'asinh', |
|||
'atan', |
|||
'atanh', |
|||
'conjugate', |
|||
'cos', |
|||
'cosh', |
|||
'cot', |
|||
'coth', |
|||
'csc', |
|||
'csch', |
|||
'exp', |
|||
'inverse', |
|||
'ln', |
|||
'log2', |
|||
'log10', |
|||
'negative', |
|||
'pow', |
|||
'rho', |
|||
'sec', |
|||
'sech', |
|||
'sin', |
|||
'sinh', |
|||
'sqrt', |
|||
'tan', |
|||
'tanh', |
|||
'theta', |
|||
]; |
|||
|
|||
protected static $operations = [ |
|||
'add', |
|||
'subtract', |
|||
'multiply', |
|||
'divideby', |
|||
'divideinto', |
|||
]; |
|||
|
|||
/** |
|||
* Returns the result of the function call or operation |
|||
* |
|||
* @return Complex|float |
|||
* @throws Exception|\InvalidArgumentException |
|||
*/ |
|||
public function __call($functionName, $arguments) |
|||
{ |
|||
$functionName = strtolower(str_replace('_', '', $functionName)); |
|||
|
|||
// Test for function calls |
|||
if (in_array($functionName, self::$functions, true)) { |
|||
return Functions::$functionName($this, ...$arguments); |
|||
} |
|||
// Test for operation calls |
|||
if (in_array($functionName, self::$operations, true)) { |
|||
return Operations::$functionName($this, ...$arguments); |
|||
} |
|||
throw new Exception('Complex Function or Operation does not exist'); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* Exception. |
|||
* |
|||
* @copyright Copyright (c) 2013-2018 Mark Baker (https://github.com/MarkBaker/PHPComplex) |
|||
* @license https://opensource.org/licenses/MIT MIT |
|||
*/ |
|||
namespace Complex; |
|||
|
|||
class Exception extends \Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,805 @@ |
|||
<?php |
|||
|
|||
namespace Complex; |
|||
|
|||
use InvalidArgumentException; |
|||
|
|||
class Functions |
|||
{ |
|||
/** |
|||
* Returns the absolute value (modulus) of a complex number. |
|||
* Also known as the rho of the complex number, i.e. the distance/radius |
|||
* from the centrepoint to the representation of the number in polar coordinates. |
|||
* |
|||
* This function is a synonym for rho() |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return float The absolute (or rho) value of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* |
|||
* @see rho |
|||
* |
|||
*/ |
|||
public static function abs($complex): float |
|||
{ |
|||
return self::rho($complex); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse cosine of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse cosine of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function acos($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
$square = clone $complex; |
|||
$square = Operations::multiply($square, $complex); |
|||
$invsqrt = new Complex(1.0); |
|||
$invsqrt = Operations::subtract($invsqrt, $square); |
|||
$invsqrt = self::sqrt($invsqrt); |
|||
$adjust = new Complex( |
|||
$complex->getReal() - $invsqrt->getImaginary(), |
|||
$complex->getImaginary() + $invsqrt->getReal() |
|||
); |
|||
$log = self::ln($adjust); |
|||
|
|||
return new Complex( |
|||
$log->getImaginary(), |
|||
-1 * $log->getReal() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse hyperbolic cosine of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse hyperbolic cosine of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function acosh($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal() && ($complex->getReal() > 1)) { |
|||
return new Complex(\acosh($complex->getReal())); |
|||
} |
|||
|
|||
$acosh = self::acos($complex) |
|||
->reverse(); |
|||
if ($acosh->getReal() < 0.0) { |
|||
$acosh = $acosh->invertReal(); |
|||
} |
|||
|
|||
return $acosh; |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse cotangent of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse cotangent of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function acot($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
return self::atan(self::inverse($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse hyperbolic cotangent of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse hyperbolic cotangent of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function acoth($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
return self::atanh(self::inverse($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse cosecant of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse cosecant of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function acsc($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
return new Complex(INF); |
|||
} |
|||
|
|||
return self::asin(self::inverse($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse hyperbolic cosecant of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse hyperbolic cosecant of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function acsch($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
return new Complex(INF); |
|||
} |
|||
|
|||
return self::asinh(self::inverse($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the argument of a complex number. |
|||
* Also known as the theta of the complex number, i.e. the angle in radians |
|||
* from the real axis to the representation of the number in polar coordinates. |
|||
* |
|||
* This function is a synonym for theta() |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return float The argument (or theta) value of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* |
|||
* @see theta |
|||
*/ |
|||
public static function argument($complex): float |
|||
{ |
|||
return self::theta($complex); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse secant of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse secant of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function asec($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
return new Complex(INF); |
|||
} |
|||
|
|||
return self::acos(self::inverse($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse hyperbolic secant of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse hyperbolic secant of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function asech($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
return new Complex(INF); |
|||
} |
|||
|
|||
return self::acosh(self::inverse($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse sine of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse sine of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function asin($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
$square = Operations::multiply($complex, $complex); |
|||
$invsqrt = new Complex(1.0); |
|||
$invsqrt = Operations::subtract($invsqrt, $square); |
|||
$invsqrt = self::sqrt($invsqrt); |
|||
$adjust = new Complex( |
|||
$invsqrt->getReal() - $complex->getImaginary(), |
|||
$invsqrt->getImaginary() + $complex->getReal() |
|||
); |
|||
$log = self::ln($adjust); |
|||
|
|||
return new Complex( |
|||
$log->getImaginary(), |
|||
-1 * $log->getReal() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse hyperbolic sine of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse hyperbolic sine of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function asinh($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal() && ($complex->getReal() > 1)) { |
|||
return new Complex(\asinh($complex->getReal())); |
|||
} |
|||
|
|||
$asinh = clone $complex; |
|||
$asinh = $asinh->reverse() |
|||
->invertReal(); |
|||
$asinh = self::asin($asinh); |
|||
|
|||
return $asinh->reverse() |
|||
->invertImaginary(); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse tangent of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse tangent of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function atan($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal()) { |
|||
return new Complex(\atan($complex->getReal())); |
|||
} |
|||
|
|||
$t1Value = new Complex(-1 * $complex->getImaginary(), $complex->getReal()); |
|||
$uValue = new Complex(1, 0); |
|||
|
|||
$d1Value = clone $uValue; |
|||
$d1Value = Operations::subtract($d1Value, $t1Value); |
|||
$d2Value = Operations::add($t1Value, $uValue); |
|||
$uResult = $d1Value->divideBy($d2Value); |
|||
$uResult = self::ln($uResult); |
|||
|
|||
return new Complex( |
|||
(($uResult->getImaginary() == M_PI) ? -M_PI : $uResult->getImaginary()) * -0.5, |
|||
$uResult->getReal() * 0.5, |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse hyperbolic tangent of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse hyperbolic tangent of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function atanh($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal()) { |
|||
$real = $complex->getReal(); |
|||
if ($real >= -1.0 && $real <= 1.0) { |
|||
return new Complex(\atanh($real)); |
|||
} else { |
|||
return new Complex(\atanh(1 / $real), (($real < 0.0) ? M_PI_2 : -1 * M_PI_2)); |
|||
} |
|||
} |
|||
|
|||
$iComplex = clone $complex; |
|||
$iComplex = $iComplex->invertImaginary() |
|||
->reverse(); |
|||
return self::atan($iComplex) |
|||
->invertReal() |
|||
->reverse(); |
|||
} |
|||
|
|||
/** |
|||
* Returns the complex conjugate of a complex number |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The conjugate of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function conjugate($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
return new Complex( |
|||
$complex->getReal(), |
|||
-1 * $complex->getImaginary(), |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the cosine of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The cosine of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function cos($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal()) { |
|||
return new Complex(\cos($complex->getReal())); |
|||
} |
|||
|
|||
return self::conjugate( |
|||
new Complex( |
|||
\cos($complex->getReal()) * \cosh($complex->getImaginary()), |
|||
\sin($complex->getReal()) * \sinh($complex->getImaginary()), |
|||
$complex->getSuffix() |
|||
) |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the hyperbolic cosine of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The hyperbolic cosine of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function cosh($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal()) { |
|||
return new Complex(\cosh($complex->getReal())); |
|||
} |
|||
|
|||
return new Complex( |
|||
\cosh($complex->getReal()) * \cos($complex->getImaginary()), |
|||
\sinh($complex->getReal()) * \sin($complex->getImaginary()), |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the cotangent of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The cotangent of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function cot($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
return new Complex(INF); |
|||
} |
|||
|
|||
return self::inverse(self::tan($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the hyperbolic cotangent of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The hyperbolic cotangent of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function coth($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
return self::inverse(self::tanh($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the cosecant of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The cosecant of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function csc($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
return new Complex(INF); |
|||
} |
|||
|
|||
return self::inverse(self::sin($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the hyperbolic cosecant of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The hyperbolic cosecant of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function csch($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
return new Complex(INF); |
|||
} |
|||
|
|||
return self::inverse(self::sinh($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the exponential of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The exponential of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function exp($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if (($complex->getReal() == 0.0) && (\abs($complex->getImaginary()) == M_PI)) { |
|||
return new Complex(-1.0, 0.0); |
|||
} |
|||
|
|||
$rho = \exp($complex->getReal()); |
|||
|
|||
return new Complex( |
|||
$rho * \cos($complex->getImaginary()), |
|||
$rho * \sin($complex->getImaginary()), |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the inverse of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The inverse of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function inverse($complex): Complex |
|||
{ |
|||
$complex = clone Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
throw new InvalidArgumentException('Division by zero'); |
|||
} |
|||
|
|||
return $complex->divideInto(1.0); |
|||
} |
|||
|
|||
/** |
|||
* Returns the natural logarithm of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The natural logarithm of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws InvalidArgumentException If the real and the imaginary parts are both zero |
|||
*/ |
|||
public static function ln($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if (($complex->getReal() == 0.0) && ($complex->getImaginary() == 0.0)) { |
|||
throw new InvalidArgumentException(); |
|||
} |
|||
|
|||
return new Complex( |
|||
\log(self::rho($complex)), |
|||
self::theta($complex), |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the base-2 logarithm of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The base-2 logarithm of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws InvalidArgumentException If the real and the imaginary parts are both zero |
|||
*/ |
|||
public static function log2($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if (($complex->getReal() == 0.0) && ($complex->getImaginary() == 0.0)) { |
|||
throw new InvalidArgumentException(); |
|||
} elseif (($complex->getReal() > 0.0) && ($complex->getImaginary() == 0.0)) { |
|||
return new Complex(\log($complex->getReal(), 2), 0.0, $complex->getSuffix()); |
|||
} |
|||
|
|||
return self::ln($complex) |
|||
->multiply(\log(Complex::EULER, 2)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the common logarithm (base 10) of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The common logarithm (base 10) of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws InvalidArgumentException If the real and the imaginary parts are both zero |
|||
*/ |
|||
public static function log10($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if (($complex->getReal() == 0.0) && ($complex->getImaginary() == 0.0)) { |
|||
throw new InvalidArgumentException(); |
|||
} elseif (($complex->getReal() > 0.0) && ($complex->getImaginary() == 0.0)) { |
|||
return new Complex(\log10($complex->getReal()), 0.0, $complex->getSuffix()); |
|||
} |
|||
|
|||
return self::ln($complex) |
|||
->multiply(\log10(Complex::EULER)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the negative of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The negative value of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* |
|||
* @see rho |
|||
* |
|||
*/ |
|||
public static function negative($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
return new Complex( |
|||
-1 * $complex->getReal(), |
|||
-1 * $complex->getImaginary(), |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns a complex number raised to a power. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @param float|integer $power The power to raise this value to |
|||
* @return Complex The complex argument raised to the real power. |
|||
* @throws Exception If the power argument isn't a valid real |
|||
*/ |
|||
public static function pow($complex, $power): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if (!is_numeric($power)) { |
|||
throw new Exception('Power argument must be a real number'); |
|||
} |
|||
|
|||
if ($complex->getImaginary() == 0.0 && $complex->getReal() >= 0.0) { |
|||
return new Complex(\pow($complex->getReal(), $power)); |
|||
} |
|||
|
|||
$rValue = \sqrt(($complex->getReal() * $complex->getReal()) + ($complex->getImaginary() * $complex->getImaginary())); |
|||
$rPower = \pow($rValue, $power); |
|||
$theta = $complex->argument() * $power; |
|||
if ($theta == 0) { |
|||
return new Complex(1); |
|||
} |
|||
|
|||
return new Complex($rPower * \cos($theta), $rPower * \sin($theta), $complex->getSuffix()); |
|||
} |
|||
|
|||
/** |
|||
* Returns the rho of a complex number. |
|||
* This is the distance/radius from the centrepoint to the representation of the number in polar coordinates. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return float The rho value of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function rho($complex): float |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
return \sqrt( |
|||
($complex->getReal() * $complex->getReal()) + |
|||
($complex->getImaginary() * $complex->getImaginary()) |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the secant of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The secant of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function sec($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
return self::inverse(self::cos($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the hyperbolic secant of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The hyperbolic secant of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function sech($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
return self::inverse(self::cosh($complex)); |
|||
} |
|||
|
|||
/** |
|||
* Returns the sine of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The sine of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function sin($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal()) { |
|||
return new Complex(\sin($complex->getReal())); |
|||
} |
|||
|
|||
return new Complex( |
|||
\sin($complex->getReal()) * \cosh($complex->getImaginary()), |
|||
\cos($complex->getReal()) * \sinh($complex->getImaginary()), |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the hyperbolic sine of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The hyperbolic sine of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function sinh($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal()) { |
|||
return new Complex(\sinh($complex->getReal())); |
|||
} |
|||
|
|||
return new Complex( |
|||
\sinh($complex->getReal()) * \cos($complex->getImaginary()), |
|||
\cosh($complex->getReal()) * \sin($complex->getImaginary()), |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the square root of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The Square root of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function sqrt($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
$theta = self::theta($complex); |
|||
$delta1 = \cos($theta / 2); |
|||
$delta2 = \sin($theta / 2); |
|||
$rho = \sqrt(self::rho($complex)); |
|||
|
|||
return new Complex($delta1 * $rho, $delta2 * $rho, $complex->getSuffix()); |
|||
} |
|||
|
|||
/** |
|||
* Returns the tangent of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The tangent of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function tan($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->isReal()) { |
|||
return new Complex(\tan($complex->getReal())); |
|||
} |
|||
|
|||
$real = $complex->getReal(); |
|||
$imaginary = $complex->getImaginary(); |
|||
$divisor = 1 + \pow(\tan($real), 2) * \pow(\tanh($imaginary), 2); |
|||
if ($divisor == 0.0) { |
|||
throw new InvalidArgumentException('Division by zero'); |
|||
} |
|||
|
|||
return new Complex( |
|||
\pow(self::sech($imaginary)->getReal(), 2) * \tan($real) / $divisor, |
|||
\pow(self::sec($real)->getReal(), 2) * \tanh($imaginary) / $divisor, |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the hyperbolic tangent of a complex number. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return Complex The hyperbolic tangent of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
* @throws \InvalidArgumentException If function would result in a division by zero |
|||
*/ |
|||
public static function tanh($complex): Complex |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
$real = $complex->getReal(); |
|||
$imaginary = $complex->getImaginary(); |
|||
$divisor = \cos($imaginary) * \cos($imaginary) + \sinh($real) * \sinh($real); |
|||
if ($divisor == 0.0) { |
|||
throw new InvalidArgumentException('Division by zero'); |
|||
} |
|||
|
|||
return new Complex( |
|||
\sinh($real) * \cosh($real) / $divisor, |
|||
0.5 * \sin(2 * $imaginary) / $divisor, |
|||
$complex->getSuffix() |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Returns the theta of a complex number. |
|||
* This is the angle in radians from the real axis to the representation of the number in polar coordinates. |
|||
* |
|||
* @param Complex|mixed $complex Complex number or a numeric value. |
|||
* @return float The theta value of the complex argument. |
|||
* @throws Exception If argument isn't a valid real or complex number. |
|||
*/ |
|||
public static function theta($complex): float |
|||
{ |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($complex->getReal() == 0.0) { |
|||
if ($complex->isReal()) { |
|||
return 0.0; |
|||
} elseif ($complex->getImaginary() < 0.0) { |
|||
return M_PI / -2; |
|||
} |
|||
return M_PI / 2; |
|||
} elseif ($complex->getReal() > 0.0) { |
|||
return \atan($complex->getImaginary() / $complex->getReal()); |
|||
} elseif ($complex->getImaginary() < 0.0) { |
|||
return -(M_PI - \atan(\abs($complex->getImaginary()) / \abs($complex->getReal()))); |
|||
} |
|||
|
|||
return M_PI - \atan($complex->getImaginary() / \abs($complex->getReal())); |
|||
} |
|||
} |
|||
@ -0,0 +1,210 @@ |
|||
<?php |
|||
|
|||
namespace Complex; |
|||
|
|||
use InvalidArgumentException; |
|||
|
|||
class Operations |
|||
{ |
|||
/** |
|||
* Adds two or more complex numbers |
|||
* |
|||
* @param array of string|integer|float|Complex $complexValues The numbers to add |
|||
* @return Complex |
|||
*/ |
|||
public static function add(...$complexValues): Complex |
|||
{ |
|||
if (count($complexValues) < 2) { |
|||
throw new \Exception('This function requires at least 2 arguments'); |
|||
} |
|||
|
|||
$base = array_shift($complexValues); |
|||
$result = clone Complex::validateComplexArgument($base); |
|||
|
|||
foreach ($complexValues as $complex) { |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($result->isComplex() && $complex->isComplex() && |
|||
$result->getSuffix() !== $complex->getSuffix()) { |
|||
throw new Exception('Suffix Mismatch'); |
|||
} |
|||
|
|||
$real = $result->getReal() + $complex->getReal(); |
|||
$imaginary = $result->getImaginary() + $complex->getImaginary(); |
|||
|
|||
$result = new Complex( |
|||
$real, |
|||
$imaginary, |
|||
($imaginary == 0.0) ? null : max($result->getSuffix(), $complex->getSuffix()) |
|||
); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Divides two or more complex numbers |
|||
* |
|||
* @param array of string|integer|float|Complex $complexValues The numbers to divide |
|||
* @return Complex |
|||
*/ |
|||
public static function divideby(...$complexValues): Complex |
|||
{ |
|||
if (count($complexValues) < 2) { |
|||
throw new \Exception('This function requires at least 2 arguments'); |
|||
} |
|||
|
|||
$base = array_shift($complexValues); |
|||
$result = clone Complex::validateComplexArgument($base); |
|||
|
|||
foreach ($complexValues as $complex) { |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($result->isComplex() && $complex->isComplex() && |
|||
$result->getSuffix() !== $complex->getSuffix()) { |
|||
throw new Exception('Suffix Mismatch'); |
|||
} |
|||
if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { |
|||
throw new InvalidArgumentException('Division by zero'); |
|||
} |
|||
|
|||
$delta1 = ($result->getReal() * $complex->getReal()) + |
|||
($result->getImaginary() * $complex->getImaginary()); |
|||
$delta2 = ($result->getImaginary() * $complex->getReal()) - |
|||
($result->getReal() * $complex->getImaginary()); |
|||
$delta3 = ($complex->getReal() * $complex->getReal()) + |
|||
($complex->getImaginary() * $complex->getImaginary()); |
|||
|
|||
$real = $delta1 / $delta3; |
|||
$imaginary = $delta2 / $delta3; |
|||
|
|||
$result = new Complex( |
|||
$real, |
|||
$imaginary, |
|||
($imaginary == 0.0) ? null : max($result->getSuffix(), $complex->getSuffix()) |
|||
); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Divides two or more complex numbers |
|||
* |
|||
* @param array of string|integer|float|Complex $complexValues The numbers to divide |
|||
* @return Complex |
|||
*/ |
|||
public static function divideinto(...$complexValues): Complex |
|||
{ |
|||
if (count($complexValues) < 2) { |
|||
throw new \Exception('This function requires at least 2 arguments'); |
|||
} |
|||
|
|||
$base = array_shift($complexValues); |
|||
$result = clone Complex::validateComplexArgument($base); |
|||
|
|||
foreach ($complexValues as $complex) { |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($result->isComplex() && $complex->isComplex() && |
|||
$result->getSuffix() !== $complex->getSuffix()) { |
|||
throw new Exception('Suffix Mismatch'); |
|||
} |
|||
if ($result->getReal() == 0.0 && $result->getImaginary() == 0.0) { |
|||
throw new InvalidArgumentException('Division by zero'); |
|||
} |
|||
|
|||
$delta1 = ($complex->getReal() * $result->getReal()) + |
|||
($complex->getImaginary() * $result->getImaginary()); |
|||
$delta2 = ($complex->getImaginary() * $result->getReal()) - |
|||
($complex->getReal() * $result->getImaginary()); |
|||
$delta3 = ($result->getReal() * $result->getReal()) + |
|||
($result->getImaginary() * $result->getImaginary()); |
|||
|
|||
$real = $delta1 / $delta3; |
|||
$imaginary = $delta2 / $delta3; |
|||
|
|||
$result = new Complex( |
|||
$real, |
|||
$imaginary, |
|||
($imaginary == 0.0) ? null : max($result->getSuffix(), $complex->getSuffix()) |
|||
); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Multiplies two or more complex numbers |
|||
* |
|||
* @param array of string|integer|float|Complex $complexValues The numbers to multiply |
|||
* @return Complex |
|||
*/ |
|||
public static function multiply(...$complexValues): Complex |
|||
{ |
|||
if (count($complexValues) < 2) { |
|||
throw new \Exception('This function requires at least 2 arguments'); |
|||
} |
|||
|
|||
$base = array_shift($complexValues); |
|||
$result = clone Complex::validateComplexArgument($base); |
|||
|
|||
foreach ($complexValues as $complex) { |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($result->isComplex() && $complex->isComplex() && |
|||
$result->getSuffix() !== $complex->getSuffix()) { |
|||
throw new Exception('Suffix Mismatch'); |
|||
} |
|||
|
|||
$real = ($result->getReal() * $complex->getReal()) - |
|||
($result->getImaginary() * $complex->getImaginary()); |
|||
$imaginary = ($result->getReal() * $complex->getImaginary()) + |
|||
($result->getImaginary() * $complex->getReal()); |
|||
|
|||
$result = new Complex( |
|||
$real, |
|||
$imaginary, |
|||
($imaginary == 0.0) ? null : max($result->getSuffix(), $complex->getSuffix()) |
|||
); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Subtracts two or more complex numbers |
|||
* |
|||
* @param array of string|integer|float|Complex $complexValues The numbers to subtract |
|||
* @return Complex |
|||
*/ |
|||
public static function subtract(...$complexValues): Complex |
|||
{ |
|||
if (count($complexValues) < 2) { |
|||
throw new \Exception('This function requires at least 2 arguments'); |
|||
} |
|||
|
|||
$base = array_shift($complexValues); |
|||
$result = clone Complex::validateComplexArgument($base); |
|||
|
|||
foreach ($complexValues as $complex) { |
|||
$complex = Complex::validateComplexArgument($complex); |
|||
|
|||
if ($result->isComplex() && $complex->isComplex() && |
|||
$result->getSuffix() !== $complex->getSuffix()) { |
|||
throw new Exception('Suffix Mismatch'); |
|||
} |
|||
|
|||
$real = $result->getReal() - $complex->getReal(); |
|||
$imaginary = $result->getImaginary() - $complex->getImaginary(); |
|||
|
|||
$result = new Complex( |
|||
$real, |
|||
$imaginary, |
|||
($imaginary == 0.0) ? null : max($result->getSuffix(), $complex->getSuffix()) |
|||
); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
{ |
|||
"name": "markbaker/complex", |
|||
"type": "library", |
|||
"description": "PHP Class for working with complex numbers", |
|||
"keywords": ["complex", "mathematics"], |
|||
"homepage": "https://github.com/MarkBaker/PHPComplex", |
|||
"license": "MIT", |
|||
"authors": [ |
|||
{ |
|||
"name": "Mark Baker", |
|||
"email": "mark@lange.demon.co.uk" |
|||
} |
|||
], |
|||
"require": { |
|||
"php": "^7.2 || ^8.0" |
|||
}, |
|||
"require-dev": { |
|||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", |
|||
"squizlabs/php_codesniffer": "^3.4", |
|||
"phpcompatibility/php-compatibility": "^9.0", |
|||
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0" |
|||
}, |
|||
"autoload": { |
|||
"psr-4": { |
|||
"Complex\\": "classes/src/" |
|||
} |
|||
}, |
|||
"scripts": { |
|||
"style": "phpcs --report-width=200 --standard=PSR2 --report=summary,full classes/src/ unitTests/classes/src -n", |
|||
"versions": "phpcs --report-width=200 --standard=PHPCompatibility --report=summary,full classes/src/ --runtime-set testVersion 7.2- -n" |
|||
}, |
|||
"minimum-stability": "dev" |
|||
} |
|||
@ -0,0 +1,154 @@ |
|||
<?php |
|||
|
|||
use Complex\Complex as Complex; |
|||
|
|||
include(__DIR__ . '/../vendor/autoload.php'); |
|||
|
|||
echo 'Create', PHP_EOL; |
|||
|
|||
$x = new Complex(123); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123, 456); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(array(123,456,'j')); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex('1.23e-4--2.34e-5i'); |
|||
echo $x, PHP_EOL; |
|||
|
|||
|
|||
echo PHP_EOL, 'Add', PHP_EOL; |
|||
|
|||
$x = new Complex(123); |
|||
$x->add(456); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456); |
|||
$x->add(789.012); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->add(new Complex(-987.654, -32.1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->add(-987.654); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->add(new Complex(0, 1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->add(new Complex(0, -1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
|
|||
echo PHP_EOL, 'Subtract', PHP_EOL; |
|||
|
|||
$x = new Complex(123); |
|||
$x->subtract(456); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456); |
|||
$x->subtract(789.012); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->subtract(new Complex(-987.654, -32.1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->subtract(-987.654); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->subtract(new Complex(0, 1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->subtract(new Complex(0, -1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
|
|||
echo PHP_EOL, 'Multiply', PHP_EOL; |
|||
|
|||
$x = new Complex(123); |
|||
$x->multiply(456); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456); |
|||
$x->multiply(789.012); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->multiply(new Complex(-987.654, -32.1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->multiply(-987.654); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->multiply(new Complex(0, 1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->multiply(new Complex(0, -1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
|
|||
echo PHP_EOL, 'Divide By', PHP_EOL; |
|||
|
|||
$x = new Complex(123); |
|||
$x->divideBy(456); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456); |
|||
$x->divideBy(789.012); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->divideBy(new Complex(-987.654, -32.1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->divideBy(-987.654); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->divideBy(new Complex(0, 1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->divideBy(new Complex(0, -1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
|
|||
echo PHP_EOL, 'Divide Into', PHP_EOL; |
|||
|
|||
$x = new Complex(123); |
|||
$x->divideInto(456); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456); |
|||
$x->divideInto(789.012); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->divideInto(new Complex(-987.654, -32.1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(123.456, 78.90); |
|||
$x->divideInto(-987.654); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->divideInto(new Complex(0, 1)); |
|||
echo $x, PHP_EOL; |
|||
|
|||
$x = new Complex(-987.654, -32.1); |
|||
$x->divideInto(new Complex(0, -1)); |
|||
echo $x, PHP_EOL; |
|||
@ -0,0 +1,52 @@ |
|||
<?php |
|||
|
|||
namespace Complex; |
|||
|
|||
include(__DIR__ . '/../vendor/autoload.php'); |
|||
|
|||
echo 'Function Examples', PHP_EOL; |
|||
|
|||
$functions = array( |
|||
'abs', |
|||
'acos', |
|||
'acosh', |
|||
'acsc', |
|||
'acsch', |
|||
'argument', |
|||
'asec', |
|||
'asech', |
|||
'asin', |
|||
'asinh', |
|||
'conjugate', |
|||
'cos', |
|||
'cosh', |
|||
'csc', |
|||
'csch', |
|||
'exp', |
|||
'inverse', |
|||
'ln', |
|||
'log2', |
|||
'log10', |
|||
'rho', |
|||
'sec', |
|||
'sech', |
|||
'sin', |
|||
'sinh', |
|||
'sqrt', |
|||
'theta' |
|||
); |
|||
|
|||
for ($real = -3.5; $real <= 3.5; $real += 0.5) { |
|||
for ($imaginary = -3.5; $imaginary <= 3.5; $imaginary += 0.5) { |
|||
foreach ($functions as $function) { |
|||
$complexFunction = __NAMESPACE__ . '\\Functions::' . $function; |
|||
$complex = new Complex($real, $imaginary); |
|||
try { |
|||
echo $function, '(', $complex, ') = ', $complexFunction($complex), PHP_EOL; |
|||
} catch (\Exception $e) { |
|||
echo $function, '(', $complex, ') ERROR: ', $e->getMessage(), PHP_EOL; |
|||
} |
|||
} |
|||
echo PHP_EOL; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
use Complex\Complex as Complex; |
|||
use Complex\Operations; |
|||
|
|||
include(__DIR__ . '/../vendor/autoload.php'); |
|||
|
|||
$values = [ |
|||
new Complex(123), |
|||
new Complex(456, 123), |
|||
new Complex(0.0, 456), |
|||
]; |
|||
|
|||
foreach ($values as $value) { |
|||
echo $value, PHP_EOL; |
|||
} |
|||
|
|||
echo 'Addition', PHP_EOL; |
|||
|
|||
$result = Operations::add(...$values); |
|||
echo '=> ', $result, PHP_EOL; |
|||
|
|||
echo PHP_EOL; |
|||
|
|||
echo 'Subtraction', PHP_EOL; |
|||
|
|||
$result = Operations::subtract(...$values); |
|||
echo '=> ', $result, PHP_EOL; |
|||
|
|||
echo PHP_EOL; |
|||
|
|||
echo 'Multiplication', PHP_EOL; |
|||
|
|||
$result = Operations::multiply(...$values); |
|||
echo '=> ', $result, PHP_EOL; |
|||
@ -0,0 +1,25 @@ |
|||
The MIT License (MIT) |
|||
===================== |
|||
|
|||
Copyright © `2017` `Mark Baker` |
|||
|
|||
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,118 @@ |
|||
name: main |
|||
on: [ push, pull_request ] |
|||
jobs: |
|||
test: |
|||
runs-on: ubuntu-latest |
|||
strategy: |
|||
matrix: |
|||
php-version: |
|||
- '7.1' |
|||
- '7.2' |
|||
- '7.3' |
|||
- '7.4' |
|||
- '8.0' |
|||
- '8.1' |
|||
|
|||
name: PHP ${{ matrix.php-version }} |
|||
|
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
- name: Setup PHP, with composer and extensions |
|||
uses: shivammathur/setup-php@v2 |
|||
with: |
|||
php-version: ${{ matrix.php-version }} |
|||
extensions: ctype, dom, gd, iconv, fileinfo, libxml, mbstring, simplexml, xml, xmlreader, xmlwriter, zip, zlib |
|||
coverage: none |
|||
|
|||
- name: Get composer cache directory |
|||
id: composer-cache |
|||
run: echo "::set-output name=dir::$(composer config cache-files-dir)" |
|||
|
|||
- name: Cache composer dependencies |
|||
uses: actions/cache@v2 |
|||
with: |
|||
path: ${{ steps.composer-cache.outputs.dir }} |
|||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} |
|||
restore-keys: ${{ runner.os }}-composer- |
|||
|
|||
- name: Set composer flags |
|||
id: composer-lock |
|||
if: ${{ matrix.php-version == '8.0' || matrix.php-version == '8.1' }} |
|||
run: | |
|||
echo "::set-output name=flags::--ignore-platform-reqs" |
|||
|
|||
- name: Install dependencies |
|||
run: composer install --no-progress --prefer-dist --optimize-autoloader ${{ steps.composer-lock.outputs.flags }} |
|||
|
|||
- name: Setup problem matchers for PHP |
|||
run: echo "::add-matcher::${{ runner.tool_cache }}/php.json" |
|||
|
|||
- name: Setup problem matchers for PHPUnit |
|||
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" |
|||
|
|||
- name: Test with PHPUnit |
|||
run: ./vendor/bin/phpunit |
|||
|
|||
phpcs: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
- name: Setup PHP, with composer and extensions |
|||
uses: shivammathur/setup-php@v2 |
|||
with: |
|||
php-version: 7.4 |
|||
extensions: ctype, dom, gd, iconv, fileinfo, libxml, mbstring, simplexml, xml, xmlreader, xmlwriter, zip, zlib |
|||
coverage: none |
|||
tools: cs2pr |
|||
|
|||
- name: Get composer cache directory |
|||
id: composer-cache |
|||
run: echo "::set-output name=dir::$(composer config cache-files-dir)" |
|||
|
|||
- name: Cache composer dependencies |
|||
uses: actions/cache@v2 |
|||
with: |
|||
path: ${{ steps.composer-cache.outputs.dir }} |
|||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} |
|||
restore-keys: ${{ runner.os }}-composer- |
|||
|
|||
- name: Install dependencies |
|||
run: composer install --no-progress --prefer-dist --optimize-autoloader |
|||
|
|||
- name: Code style with PHP_CodeSniffer |
|||
run: ./vendor/bin/phpcs -q --report=checkstyle | cs2pr --graceful-warnings --colorize |
|||
|
|||
coverage: |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- name: Checkout |
|||
uses: actions/checkout@v2 |
|||
|
|||
- name: Setup PHP, with composer and extensions |
|||
uses: shivammathur/setup-php@v2 |
|||
with: |
|||
php-version: 7.4 |
|||
extensions: ctype, dom, gd, iconv, fileinfo, libxml, mbstring, simplexml, xml, xmlreader, xmlwriter, zip, zlib |
|||
coverage: pcov |
|||
|
|||
- name: Get composer cache directory |
|||
id: composer-cache |
|||
run: echo "::set-output name=dir::$(composer config cache-files-dir)" |
|||
|
|||
- name: Cache composer dependencies |
|||
uses: actions/cache@v2 |
|||
with: |
|||
path: ${{ steps.composer-cache.outputs.dir }} |
|||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} |
|||
restore-keys: ${{ runner.os }}-composer- |
|||
|
|||
- name: Install dependencies |
|||
run: composer install --no-progress --prefer-dist --optimize-autoloader |
|||
|
|||
- name: Coverage |
|||
run: | |
|||
./vendor/bin/phpunit --coverage-text |
|||
@ -0,0 +1,215 @@ |
|||
PHPMatrix |
|||
========== |
|||
|
|||
--- |
|||
|
|||
PHP Class for handling Matrices |
|||
|
|||
[](https://github.com/MarkBaker/PHPMatrix/actions) |
|||
[](https://packagist.org/packages/markbaker/matrix) |
|||
[](https://packagist.org/packages/markbaker/matrix) |
|||
[](https://packagist.org/packages/markbaker/matrix) |
|||
|
|||
|
|||
[](https://xkcd.com/184/) |
|||
|
|||
Matrix Transform |
|||
|
|||
--- |
|||
|
|||
This library currently provides the following operations: |
|||
|
|||
- addition |
|||
- direct sum |
|||
- subtraction |
|||
- multiplication |
|||
- division (using [A].[B]<sup>-1</sup>) |
|||
- division by |
|||
- division into |
|||
|
|||
together with functions for |
|||
|
|||
- adjoint |
|||
- antidiagonal |
|||
- cofactors |
|||
- determinant |
|||
- diagonal |
|||
- identity |
|||
- inverse |
|||
- minors |
|||
- trace |
|||
- transpose |
|||
- solve |
|||
|
|||
Given Matrices A and B, calculate X for A.X = B |
|||
|
|||
and classes for |
|||
|
|||
- Decomposition |
|||
- LU Decomposition with partial row pivoting, |
|||
|
|||
such that [P].[A] = [L].[U] and [A] = [P]<sup>|</sup>.[L].[U] |
|||
- QR Decomposition |
|||
|
|||
such that [A] = [Q].[R] |
|||
|
|||
## TO DO |
|||
|
|||
- power() function |
|||
- Decomposition |
|||
- Cholesky Decomposition |
|||
- EigenValue Decomposition |
|||
- EigenValues |
|||
- EigenVectors |
|||
|
|||
--- |
|||
|
|||
# Installation |
|||
|
|||
```shell |
|||
composer require markbaker/matrix:^3.0 |
|||
``` |
|||
|
|||
# Important BC Note |
|||
|
|||
If you've previously been using procedural calls to functions and operations using this library, then from version 3.0 you should use [MarkBaker/PHPMatrixFunctions](https://github.com/MarkBaker/PHPMatrixFunctions) instead (available on packagist as [markbaker/matrix-functions](https://packagist.org/packages/markbaker/matrix-functions)). |
|||
|
|||
You'll need to replace `markbaker/matrix`in your `composer.json` file with the new library, but otherwise there should be no difference in the namespacing, or in the way that you have called the Matrix functions in the past, so no actual code changes are required. |
|||
|
|||
```shell |
|||
composer require markbaker/matrix-functions:^1.0 |
|||
``` |
|||
|
|||
You should not reference this library (`markbaker/matrix`) in your `composer.json`, composer wil take care of that for you. |
|||
|
|||
# Usage |
|||
|
|||
To create a new Matrix object, provide an array as the constructor argument |
|||
|
|||
```php |
|||
$grid = [ |
|||
[16, 3, 2, 13], |
|||
[ 5, 10, 11, 8], |
|||
[ 9, 6, 7, 12], |
|||
[ 4, 15, 14, 1], |
|||
]; |
|||
|
|||
$matrix = new Matrix\Matrix($grid); |
|||
``` |
|||
The `Builder` class provides helper methods for creating specific matrices, specifically an identity matrix of a specified size; or a matrix of a specified dimensions, with every cell containing a set value. |
|||
```php |
|||
$matrix = Matrix\Builder::createFilledMatrix(1, 5, 3); |
|||
``` |
|||
Will create a matrix of 5 rows and 3 columns, filled with a `1` in every cell; while |
|||
```php |
|||
$matrix = Matrix\Builder::createIdentityMatrix(3); |
|||
``` |
|||
will create a 3x3 identity matrix. |
|||
|
|||
|
|||
Matrix objects are immutable: whenever you call a method or pass a grid to a function that returns a matrix value, a new Matrix object will be returned, and the original will remain unchanged. This also allows you to chain multiple methods as you would for a fluent interface (as long as they are methods that will return a Matrix result). |
|||
|
|||
## Performing Mathematical Operations |
|||
|
|||
To perform mathematical operations with Matrices, you can call the appropriate method against a matrix value, passing other values as arguments |
|||
|
|||
```php |
|||
$matrix1 = new Matrix\Matrix([ |
|||
[2, 7, 6], |
|||
[9, 5, 1], |
|||
[4, 3, 8], |
|||
]); |
|||
$matrix2 = new Matrix\Matrix([ |
|||
[1, 2, 3], |
|||
[4, 5, 6], |
|||
[7, 8, 9], |
|||
]); |
|||
|
|||
var_dump($matrix1->multiply($matrix2)->toArray()); |
|||
``` |
|||
or pass all values to the appropriate static method |
|||
```php |
|||
$matrix1 = new Matrix\Matrix([ |
|||
[2, 7, 6], |
|||
[9, 5, 1], |
|||
[4, 3, 8], |
|||
]); |
|||
$matrix2 = new Matrix\Matrix([ |
|||
[1, 2, 3], |
|||
[4, 5, 6], |
|||
[7, 8, 9], |
|||
]); |
|||
|
|||
var_dump(Matrix\Operations::multiply($matrix1, $matrix2)->toArray()); |
|||
``` |
|||
You can pass in the arguments as Matrix objects, or as arrays. |
|||
|
|||
If you want to perform the same operation against multiple values (e.g. to add three or more matrices), then you can pass multiple arguments to any of the operations. |
|||
|
|||
## Using functions |
|||
|
|||
When calling any of the available functions for a matrix value, you can either call the relevant method for the Matrix object |
|||
```php |
|||
$grid = [ |
|||
[16, 3, 2, 13], |
|||
[ 5, 10, 11, 8], |
|||
[ 9, 6, 7, 12], |
|||
[ 4, 15, 14, 1], |
|||
]; |
|||
|
|||
$matrix = new Matrix\Matrix($grid); |
|||
|
|||
echo $matrix->trace(); |
|||
``` |
|||
or you can call the static method, passing the Matrix object or array as an argument |
|||
```php |
|||
$grid = [ |
|||
[16, 3, 2, 13], |
|||
[ 5, 10, 11, 8], |
|||
[ 9, 6, 7, 12], |
|||
[ 4, 15, 14, 1], |
|||
]; |
|||
|
|||
$matrix = new Matrix\Matrix($grid); |
|||
echo Matrix\Functions::trace($matrix); |
|||
``` |
|||
```php |
|||
$grid = [ |
|||
[16, 3, 2, 13], |
|||
[ 5, 10, 11, 8], |
|||
[ 9, 6, 7, 12], |
|||
[ 4, 15, 14, 1], |
|||
]; |
|||
|
|||
echo Matrix\Functions::trace($grid); |
|||
``` |
|||
|
|||
## Decomposition |
|||
|
|||
The library also provides classes for matrix decomposition. You can access these using |
|||
```php |
|||
$grid = [ |
|||
[1, 2], |
|||
[3, 4], |
|||
]; |
|||
|
|||
$matrix = new Matrix\Matrix($grid); |
|||
|
|||
$decomposition = new Matrix\Decomposition\QR($matrix); |
|||
$Q = $decomposition->getQ(); |
|||
$R = $decomposition->getR(); |
|||
``` |
|||
|
|||
or alternatively us the `Decomposition` factory, identifying which form of decomposition you want to use |
|||
```php |
|||
$grid = [ |
|||
[1, 2], |
|||
[3, 4], |
|||
]; |
|||
|
|||
$matrix = new Matrix\Matrix($grid); |
|||
|
|||
$decomposition = Matrix\Decomposition\Decomposition::decomposition(Matrix\Decomposition\Decomposition::QR, $matrix); |
|||
$Q = $decomposition->getQ(); |
|||
$R = $decomposition->getR(); |
|||
``` |
|||
@ -0,0 +1,62 @@ |
|||
<?php |
|||
|
|||
# required: PHP 5.3+ and zlib extension |
|||
|
|||
// ini option check |
|||
if (ini_get('phar.readonly')) { |
|||
echo "php.ini: set the 'phar.readonly' option to 0 to enable phar creation\n"; |
|||
exit(1); |
|||
} |
|||
|
|||
// output name |
|||
$pharName = 'Matrix.phar'; |
|||
|
|||
// target folder |
|||
$sourceDir = __DIR__ . DIRECTORY_SEPARATOR . 'classes' . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR; |
|||
|
|||
// default meta information |
|||
$metaData = array( |
|||
'Author' => 'Mark Baker <mark@lange.demon.co.uk>', |
|||
'Description' => 'PHP Class for working with Matrix numbers', |
|||
'Copyright' => 'Mark Baker (c) 2013-' . date('Y'), |
|||
'Timestamp' => time(), |
|||
'Version' => '0.1.0', |
|||
'Date' => date('Y-m-d') |
|||
); |
|||
|
|||
// cleanup |
|||
if (file_exists($pharName)) { |
|||
echo "Removed: {$pharName}\n"; |
|||
unlink($pharName); |
|||
} |
|||
|
|||
echo "Building phar file...\n"; |
|||
|
|||
// the phar object |
|||
$phar = new Phar($pharName, null, 'Matrix'); |
|||
$phar->buildFromDirectory($sourceDir); |
|||
$phar->setStub( |
|||
<<<'EOT' |
|||
<?php |
|||
spl_autoload_register(function ($className) { |
|||
include 'phar://' . $className . '.php'; |
|||
}); |
|||
|
|||
try { |
|||
Phar::mapPhar(); |
|||
} catch (PharException $e) { |
|||
error_log($e->getMessage()); |
|||
exit(1); |
|||
} |
|||
|
|||
include 'phar://functions/sqrt.php'; |
|||
|
|||
__HALT_COMPILER(); |
|||
EOT |
|||
); |
|||
$phar->setMetadata($metaData); |
|||
$phar->compressFiles(Phar::GZ); |
|||
|
|||
echo "Complete.\n"; |
|||
|
|||
exit(); |
|||
@ -0,0 +1,70 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* |
|||
* Class for the creating "special" Matrices |
|||
* |
|||
* @copyright Copyright (c) 2018 Mark Baker (https://github.com/MarkBaker/PHPMatrix) |
|||
* @license https://opensource.org/licenses/MIT MIT |
|||
*/ |
|||
|
|||
namespace Matrix; |
|||
|
|||
/** |
|||
* Matrix Builder class. |
|||
* |
|||
* @package Matrix |
|||
*/ |
|||
class Builder |
|||
{ |
|||
/** |
|||
* Create a new matrix of specified dimensions, and filled with a specified value |
|||
* If the column argument isn't provided, then a square matrix will be created |
|||
* |
|||
* @param mixed $fillValue |
|||
* @param int $rows |
|||
* @param int|null $columns |
|||
* @return Matrix |
|||
* @throws Exception |
|||
*/ |
|||
public static function createFilledMatrix($fillValue, $rows, $columns = null) |
|||
{ |
|||
if ($columns === null) { |
|||
$columns = $rows; |
|||
} |
|||
|
|||
$rows = Matrix::validateRow($rows); |
|||
$columns = Matrix::validateColumn($columns); |
|||
|
|||
return new Matrix( |
|||
array_fill( |
|||
0, |
|||
$rows, |
|||
array_fill( |
|||
0, |
|||
$columns, |
|||
$fillValue |
|||
) |
|||
) |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Create a new identity matrix of specified dimensions |
|||
* This will always be a square matrix, with the number of rows and columns matching the provided dimension |
|||
* |
|||
* @param int $dimensions |
|||
* @return Matrix |
|||
* @throws Exception |
|||
*/ |
|||
public static function createIdentityMatrix($dimensions, $fillValue = null) |
|||
{ |
|||
$grid = static::createFilledMatrix($fillValue, $dimensions)->toArray(); |
|||
|
|||
for ($x = 0; $x < $dimensions; ++$x) { |
|||
$grid[$x][$x] = 1; |
|||
} |
|||
|
|||
return new Matrix($grid); |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Decomposition; |
|||
|
|||
use Matrix\Exception; |
|||
use Matrix\Matrix; |
|||
|
|||
class Decomposition |
|||
{ |
|||
const LU = 'LU'; |
|||
const QR = 'QR'; |
|||
|
|||
/** |
|||
* @throws Exception |
|||
*/ |
|||
public static function decomposition($type, Matrix $matrix) |
|||
{ |
|||
switch (strtoupper($type)) { |
|||
case self::LU: |
|||
return new LU($matrix); |
|||
case self::QR: |
|||
return new QR($matrix); |
|||
default: |
|||
throw new Exception('Invalid Decomposition'); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,260 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Decomposition; |
|||
|
|||
use Matrix\Exception; |
|||
use Matrix\Matrix; |
|||
|
|||
class LU |
|||
{ |
|||
private $luMatrix; |
|||
private $rows; |
|||
private $columns; |
|||
|
|||
private $pivot = []; |
|||
|
|||
public function __construct(Matrix $matrix) |
|||
{ |
|||
$this->luMatrix = $matrix->toArray(); |
|||
$this->rows = $matrix->rows; |
|||
$this->columns = $matrix->columns; |
|||
|
|||
$this->buildPivot(); |
|||
} |
|||
|
|||
/** |
|||
* Get lower triangular factor. |
|||
* |
|||
* @return Matrix Lower triangular factor |
|||
*/ |
|||
public function getL(): Matrix |
|||
{ |
|||
$lower = []; |
|||
|
|||
$columns = min($this->rows, $this->columns); |
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
for ($column = 0; $column < $columns; ++$column) { |
|||
if ($row > $column) { |
|||
$lower[$row][$column] = $this->luMatrix[$row][$column]; |
|||
} elseif ($row === $column) { |
|||
$lower[$row][$column] = 1.0; |
|||
} else { |
|||
$lower[$row][$column] = 0.0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new Matrix($lower); |
|||
} |
|||
|
|||
/** |
|||
* Get upper triangular factor. |
|||
* |
|||
* @return Matrix Upper triangular factor |
|||
*/ |
|||
public function getU(): Matrix |
|||
{ |
|||
$upper = []; |
|||
|
|||
$rows = min($this->rows, $this->columns); |
|||
for ($row = 0; $row < $rows; ++$row) { |
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
if ($row <= $column) { |
|||
$upper[$row][$column] = $this->luMatrix[$row][$column]; |
|||
} else { |
|||
$upper[$row][$column] = 0.0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new Matrix($upper); |
|||
} |
|||
|
|||
/** |
|||
* Return pivot permutation vector. |
|||
* |
|||
* @return Matrix Pivot matrix |
|||
*/ |
|||
public function getP(): Matrix |
|||
{ |
|||
$pMatrix = []; |
|||
|
|||
$pivots = $this->pivot; |
|||
$pivotCount = count($pivots); |
|||
foreach ($pivots as $row => $pivot) { |
|||
$pMatrix[$row] = array_fill(0, $pivotCount, 0); |
|||
$pMatrix[$row][$pivot] = 1; |
|||
} |
|||
|
|||
return new Matrix($pMatrix); |
|||
} |
|||
|
|||
/** |
|||
* Return pivot permutation vector. |
|||
* |
|||
* @return array Pivot vector |
|||
*/ |
|||
public function getPivot(): array |
|||
{ |
|||
return $this->pivot; |
|||
} |
|||
|
|||
/** |
|||
* Is the matrix nonsingular? |
|||
* |
|||
* @return bool true if U, and hence A, is nonsingular |
|||
*/ |
|||
public function isNonsingular(): bool |
|||
{ |
|||
for ($diagonal = 0; $diagonal < $this->columns; ++$diagonal) { |
|||
if ($this->luMatrix[$diagonal][$diagonal] === 0.0) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private function buildPivot(): void |
|||
{ |
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
$this->pivot[$row] = $row; |
|||
} |
|||
|
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
$luColumn = $this->localisedReferenceColumn($column); |
|||
|
|||
$this->applyTransformations($column, $luColumn); |
|||
|
|||
$pivot = $this->findPivot($column, $luColumn); |
|||
if ($pivot !== $column) { |
|||
$this->pivotExchange($pivot, $column); |
|||
} |
|||
|
|||
$this->computeMultipliers($column); |
|||
|
|||
unset($luColumn); |
|||
} |
|||
} |
|||
|
|||
private function localisedReferenceColumn($column): array |
|||
{ |
|||
$luColumn = []; |
|||
|
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
$luColumn[$row] = &$this->luMatrix[$row][$column]; |
|||
} |
|||
|
|||
return $luColumn; |
|||
} |
|||
|
|||
private function applyTransformations($column, array $luColumn): void |
|||
{ |
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
$luRow = $this->luMatrix[$row]; |
|||
// Most of the time is spent in the following dot product. |
|||
$kmax = min($row, $column); |
|||
$sValue = 0.0; |
|||
for ($kValue = 0; $kValue < $kmax; ++$kValue) { |
|||
$sValue += $luRow[$kValue] * $luColumn[$kValue]; |
|||
} |
|||
$luRow[$column] = $luColumn[$row] -= $sValue; |
|||
} |
|||
} |
|||
|
|||
private function findPivot($column, array $luColumn): int |
|||
{ |
|||
$pivot = $column; |
|||
for ($row = $column + 1; $row < $this->rows; ++$row) { |
|||
if (abs($luColumn[$row]) > abs($luColumn[$pivot])) { |
|||
$pivot = $row; |
|||
} |
|||
} |
|||
|
|||
return $pivot; |
|||
} |
|||
|
|||
private function pivotExchange($pivot, $column): void |
|||
{ |
|||
for ($kValue = 0; $kValue < $this->columns; ++$kValue) { |
|||
$tValue = $this->luMatrix[$pivot][$kValue]; |
|||
$this->luMatrix[$pivot][$kValue] = $this->luMatrix[$column][$kValue]; |
|||
$this->luMatrix[$column][$kValue] = $tValue; |
|||
} |
|||
|
|||
$lValue = $this->pivot[$pivot]; |
|||
$this->pivot[$pivot] = $this->pivot[$column]; |
|||
$this->pivot[$column] = $lValue; |
|||
} |
|||
|
|||
private function computeMultipliers($diagonal): void |
|||
{ |
|||
if (($diagonal < $this->rows) && ($this->luMatrix[$diagonal][$diagonal] != 0.0)) { |
|||
for ($row = $diagonal + 1; $row < $this->rows; ++$row) { |
|||
$this->luMatrix[$row][$diagonal] /= $this->luMatrix[$diagonal][$diagonal]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
private function pivotB(Matrix $B): array |
|||
{ |
|||
$X = []; |
|||
foreach ($this->pivot as $rowId) { |
|||
$row = $B->getRows($rowId + 1)->toArray(); |
|||
$X[] = array_pop($row); |
|||
} |
|||
|
|||
return $X; |
|||
} |
|||
|
|||
/** |
|||
* Solve A*X = B. |
|||
* |
|||
* @param Matrix $B a Matrix with as many rows as A and any number of columns |
|||
* |
|||
* @throws Exception |
|||
* |
|||
* @return Matrix X so that L*U*X = B(piv,:) |
|||
*/ |
|||
public function solve(Matrix $B): Matrix |
|||
{ |
|||
if ($B->rows !== $this->rows) { |
|||
throw new Exception('Matrix row dimensions are not equal'); |
|||
} |
|||
|
|||
if ($this->rows !== $this->columns) { |
|||
throw new Exception('LU solve() only works on square matrices'); |
|||
} |
|||
|
|||
if (!$this->isNonsingular()) { |
|||
throw new Exception('Can only perform operation on singular matrix'); |
|||
} |
|||
|
|||
// Copy right hand side with pivoting |
|||
$nx = $B->columns; |
|||
$X = $this->pivotB($B); |
|||
|
|||
// Solve L*Y = B(piv,:) |
|||
for ($k = 0; $k < $this->columns; ++$k) { |
|||
for ($i = $k + 1; $i < $this->columns; ++$i) { |
|||
for ($j = 0; $j < $nx; ++$j) { |
|||
$X[$i][$j] -= $X[$k][$j] * $this->luMatrix[$i][$k]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Solve U*X = Y; |
|||
for ($k = $this->columns - 1; $k >= 0; --$k) { |
|||
for ($j = 0; $j < $nx; ++$j) { |
|||
$X[$k][$j] /= $this->luMatrix[$k][$k]; |
|||
} |
|||
for ($i = 0; $i < $k; ++$i) { |
|||
for ($j = 0; $j < $nx; ++$j) { |
|||
$X[$i][$j] -= $X[$k][$j] * $this->luMatrix[$i][$k]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new Matrix($X); |
|||
} |
|||
} |
|||
@ -0,0 +1,191 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Decomposition; |
|||
|
|||
use Matrix\Exception; |
|||
use Matrix\Matrix; |
|||
|
|||
class QR |
|||
{ |
|||
private $qrMatrix; |
|||
private $rows; |
|||
private $columns; |
|||
|
|||
private $rDiagonal = []; |
|||
|
|||
public function __construct(Matrix $matrix) |
|||
{ |
|||
$this->qrMatrix = $matrix->toArray(); |
|||
$this->rows = $matrix->rows; |
|||
$this->columns = $matrix->columns; |
|||
|
|||
$this->decompose(); |
|||
} |
|||
|
|||
public function getHouseholdVectors(): Matrix |
|||
{ |
|||
$householdVectors = []; |
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
if ($row >= $column) { |
|||
$householdVectors[$row][$column] = $this->qrMatrix[$row][$column]; |
|||
} else { |
|||
$householdVectors[$row][$column] = 0.0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return new Matrix($householdVectors); |
|||
} |
|||
|
|||
public function getQ(): Matrix |
|||
{ |
|||
$qGrid = []; |
|||
|
|||
$rowCount = $this->rows; |
|||
for ($k = $this->columns - 1; $k >= 0; --$k) { |
|||
for ($i = 0; $i < $this->rows; ++$i) { |
|||
$qGrid[$i][$k] = 0.0; |
|||
} |
|||
$qGrid[$k][$k] = 1.0; |
|||
if ($this->columns > $this->rows) { |
|||
$qGrid = array_slice($qGrid, 0, $this->rows); |
|||
} |
|||
|
|||
for ($j = $k; $j < $this->columns; ++$j) { |
|||
if (isset($this->qrMatrix[$k], $this->qrMatrix[$k][$k]) && $this->qrMatrix[$k][$k] != 0.0) { |
|||
$s = 0.0; |
|||
for ($i = $k; $i < $this->rows; ++$i) { |
|||
$s += $this->qrMatrix[$i][$k] * $qGrid[$i][$j]; |
|||
} |
|||
$s = -$s / $this->qrMatrix[$k][$k]; |
|||
for ($i = $k; $i < $this->rows; ++$i) { |
|||
$qGrid[$i][$j] += $s * $this->qrMatrix[$i][$k]; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
array_walk( |
|||
$qGrid, |
|||
function (&$row) use ($rowCount) { |
|||
$row = array_reverse($row); |
|||
$row = array_slice($row, 0, $rowCount); |
|||
} |
|||
); |
|||
|
|||
return new Matrix($qGrid); |
|||
} |
|||
|
|||
public function getR(): Matrix |
|||
{ |
|||
$rGrid = []; |
|||
|
|||
for ($row = 0; $row < $this->columns; ++$row) { |
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
if ($row < $column) { |
|||
$rGrid[$row][$column] = $this->qrMatrix[$row][$column] ?? 0.0; |
|||
} elseif ($row === $column) { |
|||
$rGrid[$row][$column] = $this->rDiagonal[$row] ?? 0.0; |
|||
} else { |
|||
$rGrid[$row][$column] = 0.0; |
|||
} |
|||
} |
|||
} |
|||
|
|||
if ($this->columns > $this->rows) { |
|||
$rGrid = array_slice($rGrid, 0, $this->rows); |
|||
} |
|||
|
|||
return new Matrix($rGrid); |
|||
} |
|||
|
|||
private function hypo($a, $b): float |
|||
{ |
|||
if (abs($a) > abs($b)) { |
|||
$r = $b / $a; |
|||
$r = abs($a) * sqrt(1 + $r * $r); |
|||
} elseif ($b != 0.0) { |
|||
$r = $a / $b; |
|||
$r = abs($b) * sqrt(1 + $r * $r); |
|||
} else { |
|||
$r = 0.0; |
|||
} |
|||
|
|||
return $r; |
|||
} |
|||
|
|||
/** |
|||
* QR Decomposition computed by Householder reflections. |
|||
*/ |
|||
private function decompose(): void |
|||
{ |
|||
for ($k = 0; $k < $this->columns; ++$k) { |
|||
// Compute 2-norm of k-th column without under/overflow. |
|||
$norm = 0.0; |
|||
for ($i = $k; $i < $this->rows; ++$i) { |
|||
$norm = $this->hypo($norm, $this->qrMatrix[$i][$k]); |
|||
} |
|||
if ($norm != 0.0) { |
|||
// Form k-th Householder vector. |
|||
if ($this->qrMatrix[$k][$k] < 0.0) { |
|||
$norm = -$norm; |
|||
} |
|||
for ($i = $k; $i < $this->rows; ++$i) { |
|||
$this->qrMatrix[$i][$k] /= $norm; |
|||
} |
|||
$this->qrMatrix[$k][$k] += 1.0; |
|||
// Apply transformation to remaining columns. |
|||
for ($j = $k + 1; $j < $this->columns; ++$j) { |
|||
$s = 0.0; |
|||
for ($i = $k; $i < $this->rows; ++$i) { |
|||
$s += $this->qrMatrix[$i][$k] * $this->qrMatrix[$i][$j]; |
|||
} |
|||
$s = -$s / $this->qrMatrix[$k][$k]; |
|||
for ($i = $k; $i < $this->rows; ++$i) { |
|||
$this->qrMatrix[$i][$j] += $s * $this->qrMatrix[$i][$k]; |
|||
} |
|||
} |
|||
} |
|||
$this->rDiagonal[$k] = -$norm; |
|||
} |
|||
} |
|||
|
|||
public function isFullRank(): bool |
|||
{ |
|||
for ($j = 0; $j < $this->columns; ++$j) { |
|||
if ($this->rDiagonal[$j] == 0.0) { |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/** |
|||
* Least squares solution of A*X = B. |
|||
* |
|||
* @param Matrix $B a Matrix with as many rows as A and any number of columns |
|||
* |
|||
* @throws Exception |
|||
* |
|||
* @return Matrix matrix that minimizes the two norm of Q*R*X-B |
|||
*/ |
|||
public function solve(Matrix $B): Matrix |
|||
{ |
|||
if ($B->rows !== $this->rows) { |
|||
throw new Exception('Matrix row dimensions are not equal'); |
|||
} |
|||
|
|||
if (!$this->isFullRank()) { |
|||
throw new Exception('Can only perform this operation on a full-rank matrix'); |
|||
} |
|||
|
|||
// Compute Y = transpose(Q)*B |
|||
$Y = $this->getQ()->transpose() |
|||
->multiply($B); |
|||
// Solve R*X = Y; |
|||
return $this->getR()->inverse() |
|||
->multiply($Y); |
|||
} |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* Exception. |
|||
* |
|||
* @copyright Copyright (c) 2013-2018 Mark Baker (https://github.com/MarkBaker/PHPMatrix) |
|||
* @license https://opensource.org/licenses/MIT MIT |
|||
*/ |
|||
namespace Matrix; |
|||
|
|||
class Div0Exception extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,13 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* Exception. |
|||
* |
|||
* @copyright Copyright (c) 2013-2018 Mark Baker (https://github.com/MarkBaker/PHPMatrix) |
|||
* @license https://opensource.org/licenses/MIT MIT |
|||
*/ |
|||
namespace Matrix; |
|||
|
|||
class Exception extends \Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,376 @@ |
|||
<?php |
|||
|
|||
namespace Matrix; |
|||
|
|||
class Functions |
|||
{ |
|||
/** |
|||
* Validates an array of matrix, converting an array to a matrix if required. |
|||
* |
|||
* @param Matrix|array $matrix Matrix or an array to treat as a matrix. |
|||
* @return Matrix The new matrix |
|||
* @throws Exception If argument isn't a valid matrix or array. |
|||
*/ |
|||
private static function validateMatrix($matrix) |
|||
{ |
|||
if (is_array($matrix)) { |
|||
$matrix = new Matrix($matrix); |
|||
} |
|||
if (!$matrix instanceof Matrix) { |
|||
throw new Exception('Must be Matrix or array'); |
|||
} |
|||
|
|||
return $matrix; |
|||
} |
|||
|
|||
/** |
|||
* Calculate the adjoint of the matrix |
|||
* |
|||
* @param Matrix $matrix The matrix whose adjoint we wish to calculate |
|||
* @return Matrix |
|||
* |
|||
* @throws Exception |
|||
*/ |
|||
private static function getAdjoint(Matrix $matrix) |
|||
{ |
|||
return self::transpose( |
|||
self::getCofactors($matrix) |
|||
); |
|||
} |
|||
|
|||
/** |
|||
* Return the adjoint of this matrix |
|||
* The adjugate, classical adjoint, or adjunct of a square matrix is the transpose of its cofactor matrix. |
|||
* The adjugate has sometimes been called the "adjoint", but today the "adjoint" of a matrix normally refers |
|||
* to its corresponding adjoint operator, which is its conjugate transpose. |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose adjoint we wish to calculate |
|||
* @return Matrix |
|||
* @throws Exception |
|||
**/ |
|||
public static function adjoint($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception('Adjoint can only be calculated for a square matrix'); |
|||
} |
|||
|
|||
return self::getAdjoint($matrix); |
|||
} |
|||
|
|||
/** |
|||
* Calculate the cofactors of the matrix |
|||
* |
|||
* @param Matrix $matrix The matrix whose cofactors we wish to calculate |
|||
* @return Matrix |
|||
* |
|||
* @throws Exception |
|||
*/ |
|||
private static function getCofactors(Matrix $matrix) |
|||
{ |
|||
$cofactors = self::getMinors($matrix); |
|||
$dimensions = $matrix->rows; |
|||
|
|||
$cof = 1; |
|||
for ($i = 0; $i < $dimensions; ++$i) { |
|||
$cofs = $cof; |
|||
for ($j = 0; $j < $dimensions; ++$j) { |
|||
$cofactors[$i][$j] *= $cofs; |
|||
$cofs = -$cofs; |
|||
} |
|||
$cof = -$cof; |
|||
} |
|||
|
|||
return new Matrix($cofactors); |
|||
} |
|||
|
|||
/** |
|||
* Return the cofactors of this matrix |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose cofactors we wish to calculate |
|||
* @return Matrix |
|||
* |
|||
* @throws Exception |
|||
*/ |
|||
public static function cofactors($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception('Cofactors can only be calculated for a square matrix'); |
|||
} |
|||
|
|||
return self::getCofactors($matrix); |
|||
} |
|||
|
|||
/** |
|||
* @param Matrix $matrix |
|||
* @param int $row |
|||
* @param int $column |
|||
* @return float |
|||
* @throws Exception |
|||
*/ |
|||
private static function getDeterminantSegment(Matrix $matrix, $row, $column) |
|||
{ |
|||
$tmpMatrix = $matrix->toArray(); |
|||
unset($tmpMatrix[$row]); |
|||
array_walk( |
|||
$tmpMatrix, |
|||
function (&$row) use ($column) { |
|||
unset($row[$column]); |
|||
} |
|||
); |
|||
|
|||
return self::getDeterminant(new Matrix($tmpMatrix)); |
|||
} |
|||
|
|||
/** |
|||
* Calculate the determinant of the matrix |
|||
* |
|||
* @param Matrix $matrix The matrix whose determinant we wish to calculate |
|||
* @return float |
|||
* |
|||
* @throws Exception |
|||
*/ |
|||
private static function getDeterminant(Matrix $matrix) |
|||
{ |
|||
$dimensions = $matrix->rows; |
|||
$determinant = 0; |
|||
|
|||
switch ($dimensions) { |
|||
case 1: |
|||
$determinant = $matrix->getValue(1, 1); |
|||
break; |
|||
case 2: |
|||
$determinant = $matrix->getValue(1, 1) * $matrix->getValue(2, 2) - |
|||
$matrix->getValue(1, 2) * $matrix->getValue(2, 1); |
|||
break; |
|||
default: |
|||
for ($i = 1; $i <= $dimensions; ++$i) { |
|||
$det = $matrix->getValue(1, $i) * self::getDeterminantSegment($matrix, 0, $i - 1); |
|||
if (($i % 2) == 0) { |
|||
$determinant -= $det; |
|||
} else { |
|||
$determinant += $det; |
|||
} |
|||
} |
|||
break; |
|||
} |
|||
|
|||
return $determinant; |
|||
} |
|||
|
|||
/** |
|||
* Return the determinant of this matrix |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose determinant we wish to calculate |
|||
* @return float |
|||
* @throws Exception |
|||
**/ |
|||
public static function determinant($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception('Determinant can only be calculated for a square matrix'); |
|||
} |
|||
|
|||
return self::getDeterminant($matrix); |
|||
} |
|||
|
|||
/** |
|||
* Return the diagonal of this matrix |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose diagonal we wish to calculate |
|||
* @return Matrix |
|||
* @throws Exception |
|||
**/ |
|||
public static function diagonal($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception('Diagonal can only be extracted from a square matrix'); |
|||
} |
|||
|
|||
$dimensions = $matrix->rows; |
|||
$grid = Builder::createFilledMatrix(0, $dimensions, $dimensions) |
|||
->toArray(); |
|||
|
|||
for ($i = 0; $i < $dimensions; ++$i) { |
|||
$grid[$i][$i] = $matrix->getValue($i + 1, $i + 1); |
|||
} |
|||
|
|||
return new Matrix($grid); |
|||
} |
|||
|
|||
/** |
|||
* Return the antidiagonal of this matrix |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose antidiagonal we wish to calculate |
|||
* @return Matrix |
|||
* @throws Exception |
|||
**/ |
|||
public static function antidiagonal($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception('Anti-Diagonal can only be extracted from a square matrix'); |
|||
} |
|||
|
|||
$dimensions = $matrix->rows; |
|||
$grid = Builder::createFilledMatrix(0, $dimensions, $dimensions) |
|||
->toArray(); |
|||
|
|||
for ($i = 0; $i < $dimensions; ++$i) { |
|||
$grid[$i][$dimensions - $i - 1] = $matrix->getValue($i + 1, $dimensions - $i); |
|||
} |
|||
|
|||
return new Matrix($grid); |
|||
} |
|||
|
|||
/** |
|||
* Return the identity matrix |
|||
* The identity matrix, or sometimes ambiguously called a unit matrix, of size n is the n × n square matrix |
|||
* with ones on the main diagonal and zeros elsewhere |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose identity we wish to calculate |
|||
* @return Matrix |
|||
* @throws Exception |
|||
**/ |
|||
public static function identity($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception('Identity can only be created for a square matrix'); |
|||
} |
|||
|
|||
$dimensions = $matrix->rows; |
|||
|
|||
return Builder::createIdentityMatrix($dimensions); |
|||
} |
|||
|
|||
/** |
|||
* Return the inverse of this matrix |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose inverse we wish to calculate |
|||
* @return Matrix |
|||
* @throws Exception |
|||
**/ |
|||
public static function inverse($matrix, string $type = 'inverse') |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception(ucfirst($type) . ' can only be calculated for a square matrix'); |
|||
} |
|||
|
|||
$determinant = self::getDeterminant($matrix); |
|||
if ($determinant == 0.0) { |
|||
throw new Div0Exception(ucfirst($type) . ' can only be calculated for a matrix with a non-zero determinant'); |
|||
} |
|||
|
|||
if ($matrix->rows == 1) { |
|||
return new Matrix([[1 / $matrix->getValue(1, 1)]]); |
|||
} |
|||
|
|||
return self::getAdjoint($matrix) |
|||
->multiply(1 / $determinant); |
|||
} |
|||
|
|||
/** |
|||
* Calculate the minors of the matrix |
|||
* |
|||
* @param Matrix $matrix The matrix whose minors we wish to calculate |
|||
* @return array[] |
|||
* |
|||
* @throws Exception |
|||
*/ |
|||
protected static function getMinors(Matrix $matrix) |
|||
{ |
|||
$minors = $matrix->toArray(); |
|||
$dimensions = $matrix->rows; |
|||
if ($dimensions == 1) { |
|||
return $minors; |
|||
} |
|||
|
|||
for ($i = 0; $i < $dimensions; ++$i) { |
|||
for ($j = 0; $j < $dimensions; ++$j) { |
|||
$minors[$i][$j] = self::getDeterminantSegment($matrix, $i, $j); |
|||
} |
|||
} |
|||
|
|||
return $minors; |
|||
} |
|||
|
|||
/** |
|||
* Return the minors of the matrix |
|||
* The minor of a matrix A is the determinant of some smaller square matrix, cut down from A by removing one or |
|||
* more of its rows or columns. |
|||
* Minors obtained by removing just one row and one column from square matrices (first minors) are required for |
|||
* calculating matrix cofactors, which in turn are useful for computing both the determinant and inverse of |
|||
* square matrices. |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose minors we wish to calculate |
|||
* @return Matrix |
|||
* @throws Exception |
|||
**/ |
|||
public static function minors($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception('Minors can only be calculated for a square matrix'); |
|||
} |
|||
|
|||
return new Matrix(self::getMinors($matrix)); |
|||
} |
|||
|
|||
/** |
|||
* Return the trace of this matrix |
|||
* The trace is defined as the sum of the elements on the main diagonal (the diagonal from the upper left to the lower right) |
|||
* of the matrix |
|||
* |
|||
* @param Matrix|array $matrix The matrix whose trace we wish to calculate |
|||
* @return float |
|||
* @throws Exception |
|||
**/ |
|||
public static function trace($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
if (!$matrix->isSquare()) { |
|||
throw new Exception('Trace can only be extracted from a square matrix'); |
|||
} |
|||
|
|||
$dimensions = $matrix->rows; |
|||
$result = 0; |
|||
for ($i = 1; $i <= $dimensions; ++$i) { |
|||
$result += $matrix->getValue($i, $i); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Return the transpose of this matrix |
|||
* |
|||
* @param Matrix|\a $matrix The matrix whose transpose we wish to calculate |
|||
* @return Matrix |
|||
**/ |
|||
public static function transpose($matrix) |
|||
{ |
|||
$matrix = self::validateMatrix($matrix); |
|||
|
|||
$array = array_values(array_merge([null], $matrix->toArray())); |
|||
$grid = call_user_func_array( |
|||
'array_map', |
|||
$array |
|||
); |
|||
|
|||
return new Matrix($grid); |
|||
} |
|||
} |
|||
@ -0,0 +1,423 @@ |
|||
<?php |
|||
|
|||
/** |
|||
* |
|||
* Class for the management of Matrices |
|||
* |
|||
* @copyright Copyright (c) 2018 Mark Baker (https://github.com/MarkBaker/PHPMatrix) |
|||
* @license https://opensource.org/licenses/MIT MIT |
|||
*/ |
|||
|
|||
namespace Matrix; |
|||
|
|||
use Generator; |
|||
use Matrix\Decomposition\LU; |
|||
use Matrix\Decomposition\QR; |
|||
|
|||
/** |
|||
* Matrix object. |
|||
* |
|||
* @package Matrix |
|||
* |
|||
* @property-read int $rows The number of rows in the matrix |
|||
* @property-read int $columns The number of columns in the matrix |
|||
* @method Matrix antidiagonal() |
|||
* @method Matrix adjoint() |
|||
* @method Matrix cofactors() |
|||
* @method float determinant() |
|||
* @method Matrix diagonal() |
|||
* @method Matrix identity() |
|||
* @method Matrix inverse() |
|||
* @method Matrix minors() |
|||
* @method float trace() |
|||
* @method Matrix transpose() |
|||
* @method Matrix add(...$matrices) |
|||
* @method Matrix subtract(...$matrices) |
|||
* @method Matrix multiply(...$matrices) |
|||
* @method Matrix divideby(...$matrices) |
|||
* @method Matrix divideinto(...$matrices) |
|||
* @method Matrix directsum(...$matrices) |
|||
*/ |
|||
class Matrix |
|||
{ |
|||
protected $rows; |
|||
protected $columns; |
|||
protected $grid = []; |
|||
|
|||
/* |
|||
* Create a new Matrix object from an array of values |
|||
* |
|||
* @param array $grid |
|||
*/ |
|||
final public function __construct(array $grid) |
|||
{ |
|||
$this->buildFromArray(array_values($grid)); |
|||
} |
|||
|
|||
/* |
|||
* Create a new Matrix object from an array of values |
|||
* |
|||
* @param array $grid |
|||
*/ |
|||
protected function buildFromArray(array $grid): void |
|||
{ |
|||
$this->rows = count($grid); |
|||
$columns = array_reduce( |
|||
$grid, |
|||
function ($carry, $value) { |
|||
return max($carry, is_array($value) ? count($value) : 1); |
|||
} |
|||
); |
|||
$this->columns = $columns; |
|||
|
|||
array_walk( |
|||
$grid, |
|||
function (&$value) use ($columns) { |
|||
if (!is_array($value)) { |
|||
$value = [$value]; |
|||
} |
|||
$value = array_pad(array_values($value), $columns, null); |
|||
} |
|||
); |
|||
|
|||
$this->grid = $grid; |
|||
} |
|||
|
|||
/** |
|||
* Validate that a row number is a positive integer |
|||
* |
|||
* @param int $row |
|||
* @return int |
|||
* @throws Exception |
|||
*/ |
|||
public static function validateRow(int $row): int |
|||
{ |
|||
if ((!is_numeric($row)) || (intval($row) < 1)) { |
|||
throw new Exception('Invalid Row'); |
|||
} |
|||
|
|||
return (int)$row; |
|||
} |
|||
|
|||
/** |
|||
* Validate that a column number is a positive integer |
|||
* |
|||
* @param int $column |
|||
* @return int |
|||
* @throws Exception |
|||
*/ |
|||
public static function validateColumn(int $column): int |
|||
{ |
|||
if ((!is_numeric($column)) || (intval($column) < 1)) { |
|||
throw new Exception('Invalid Column'); |
|||
} |
|||
|
|||
return (int)$column; |
|||
} |
|||
|
|||
/** |
|||
* Validate that a row number falls within the set of rows for this matrix |
|||
* |
|||
* @param int $row |
|||
* @return int |
|||
* @throws Exception |
|||
*/ |
|||
protected function validateRowInRange(int $row): int |
|||
{ |
|||
$row = static::validateRow($row); |
|||
if ($row > $this->rows) { |
|||
throw new Exception('Requested Row exceeds matrix size'); |
|||
} |
|||
|
|||
return $row; |
|||
} |
|||
|
|||
/** |
|||
* Validate that a column number falls within the set of columns for this matrix |
|||
* |
|||
* @param int $column |
|||
* @return int |
|||
* @throws Exception |
|||
*/ |
|||
protected function validateColumnInRange(int $column): int |
|||
{ |
|||
$column = static::validateColumn($column); |
|||
if ($column > $this->columns) { |
|||
throw new Exception('Requested Column exceeds matrix size'); |
|||
} |
|||
|
|||
return $column; |
|||
} |
|||
|
|||
/** |
|||
* Return a new matrix as a subset of rows from this matrix, starting at row number $row, and $rowCount rows |
|||
* A $rowCount value of 0 will return all rows of the matrix from $row |
|||
* A negative $rowCount value will return rows until that many rows from the end of the matrix |
|||
* |
|||
* Note that row numbers start from 1, not from 0 |
|||
* |
|||
* @param int $row |
|||
* @param int $rowCount |
|||
* @return static |
|||
* @throws Exception |
|||
*/ |
|||
public function getRows(int $row, int $rowCount = 1): Matrix |
|||
{ |
|||
$row = $this->validateRowInRange($row); |
|||
if ($rowCount === 0) { |
|||
$rowCount = $this->rows - $row + 1; |
|||
} |
|||
|
|||
return new static(array_slice($this->grid, $row - 1, (int)$rowCount)); |
|||
} |
|||
|
|||
/** |
|||
* Return a new matrix as a subset of columns from this matrix, starting at column number $column, and $columnCount columns |
|||
* A $columnCount value of 0 will return all columns of the matrix from $column |
|||
* A negative $columnCount value will return columns until that many columns from the end of the matrix |
|||
* |
|||
* Note that column numbers start from 1, not from 0 |
|||
* |
|||
* @param int $column |
|||
* @param int $columnCount |
|||
* @return Matrix |
|||
* @throws Exception |
|||
*/ |
|||
public function getColumns(int $column, int $columnCount = 1): Matrix |
|||
{ |
|||
$column = $this->validateColumnInRange($column); |
|||
if ($columnCount < 1) { |
|||
$columnCount = $this->columns + $columnCount - $column + 1; |
|||
} |
|||
|
|||
$grid = []; |
|||
for ($i = $column - 1; $i < $column + $columnCount - 1; ++$i) { |
|||
$grid[] = array_column($this->grid, $i); |
|||
} |
|||
|
|||
return (new static($grid))->transpose(); |
|||
} |
|||
|
|||
/** |
|||
* Return a new matrix as a subset of rows from this matrix, dropping rows starting at row number $row, |
|||
* and $rowCount rows |
|||
* A negative $rowCount value will drop rows until that many rows from the end of the matrix |
|||
* A $rowCount value of 0 will remove all rows of the matrix from $row |
|||
* |
|||
* Note that row numbers start from 1, not from 0 |
|||
* |
|||
* @param int $row |
|||
* @param int $rowCount |
|||
* @return static |
|||
* @throws Exception |
|||
*/ |
|||
public function dropRows(int $row, int $rowCount = 1): Matrix |
|||
{ |
|||
$this->validateRowInRange($row); |
|||
if ($rowCount === 0) { |
|||
$rowCount = $this->rows - $row + 1; |
|||
} |
|||
|
|||
$grid = $this->grid; |
|||
array_splice($grid, $row - 1, (int)$rowCount); |
|||
|
|||
return new static($grid); |
|||
} |
|||
|
|||
/** |
|||
* Return a new matrix as a subset of columns from this matrix, dropping columns starting at column number $column, |
|||
* and $columnCount columns |
|||
* A negative $columnCount value will drop columns until that many columns from the end of the matrix |
|||
* A $columnCount value of 0 will remove all columns of the matrix from $column |
|||
* |
|||
* Note that column numbers start from 1, not from 0 |
|||
* |
|||
* @param int $column |
|||
* @param int $columnCount |
|||
* @return static |
|||
* @throws Exception |
|||
*/ |
|||
public function dropColumns(int $column, int $columnCount = 1): Matrix |
|||
{ |
|||
$this->validateColumnInRange($column); |
|||
if ($columnCount < 1) { |
|||
$columnCount = $this->columns + $columnCount - $column + 1; |
|||
} |
|||
|
|||
$grid = $this->grid; |
|||
array_walk( |
|||
$grid, |
|||
function (&$row) use ($column, $columnCount) { |
|||
array_splice($row, $column - 1, (int)$columnCount); |
|||
} |
|||
); |
|||
|
|||
return new static($grid); |
|||
} |
|||
|
|||
/** |
|||
* Return a value from this matrix, from the "cell" identified by the row and column numbers |
|||
* Note that row and column numbers start from 1, not from 0 |
|||
* |
|||
* @param int $row |
|||
* @param int $column |
|||
* @return mixed |
|||
* @throws Exception |
|||
*/ |
|||
public function getValue(int $row, int $column) |
|||
{ |
|||
$row = $this->validateRowInRange($row); |
|||
$column = $this->validateColumnInRange($column); |
|||
|
|||
return $this->grid[$row - 1][$column - 1]; |
|||
} |
|||
|
|||
/** |
|||
* Returns a Generator that will yield each row of the matrix in turn as a vector matrix |
|||
* or the value of each cell if the matrix is a column vector |
|||
* |
|||
* @return Generator|Matrix[]|mixed[] |
|||
*/ |
|||
public function rows(): Generator |
|||
{ |
|||
foreach ($this->grid as $i => $row) { |
|||
yield $i + 1 => ($this->columns == 1) |
|||
? $row[0] |
|||
: new static([$row]); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Returns a Generator that will yield each column of the matrix in turn as a vector matrix |
|||
* or the value of each cell if the matrix is a row vector |
|||
* |
|||
* @return Generator|Matrix[]|mixed[] |
|||
*/ |
|||
public function columns(): Generator |
|||
{ |
|||
for ($i = 0; $i < $this->columns; ++$i) { |
|||
yield $i + 1 => ($this->rows == 1) |
|||
? $this->grid[0][$i] |
|||
: new static(array_column($this->grid, $i)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Identify if the row and column dimensions of this matrix are equal, |
|||
* i.e. if it is a "square" matrix |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isSquare(): bool |
|||
{ |
|||
return $this->rows === $this->columns; |
|||
} |
|||
|
|||
/** |
|||
* Identify if this matrix is a vector |
|||
* i.e. if it comprises only a single row or a single column |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public function isVector(): bool |
|||
{ |
|||
return $this->rows === 1 || $this->columns === 1; |
|||
} |
|||
|
|||
/** |
|||
* Return the matrix as a 2-dimensional array |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function toArray(): array |
|||
{ |
|||
return $this->grid; |
|||
} |
|||
|
|||
/** |
|||
* Solve A*X = B. |
|||
* |
|||
* @param Matrix $B Right hand side |
|||
* |
|||
* @throws Exception |
|||
* |
|||
* @return Matrix ... Solution if A is square, least squares solution otherwise |
|||
*/ |
|||
public function solve(Matrix $B): Matrix |
|||
{ |
|||
if ($this->columns === $this->rows) { |
|||
return (new LU($this))->solve($B); |
|||
} |
|||
|
|||
return (new QR($this))->solve($B); |
|||
} |
|||
|
|||
protected static $getters = [ |
|||
'rows', |
|||
'columns', |
|||
]; |
|||
|
|||
/** |
|||
* Access specific properties as read-only (no setters) |
|||
* |
|||
* @param string $propertyName |
|||
* @return mixed |
|||
* @throws Exception |
|||
*/ |
|||
public function __get(string $propertyName) |
|||
{ |
|||
$propertyName = strtolower($propertyName); |
|||
|
|||
// Test for function calls |
|||
if (in_array($propertyName, self::$getters)) { |
|||
return $this->$propertyName; |
|||
} |
|||
|
|||
throw new Exception('Property does not exist'); |
|||
} |
|||
|
|||
protected static $functions = [ |
|||
'adjoint', |
|||
'antidiagonal', |
|||
'cofactors', |
|||
'determinant', |
|||
'diagonal', |
|||
'identity', |
|||
'inverse', |
|||
'minors', |
|||
'trace', |
|||
'transpose', |
|||
]; |
|||
|
|||
protected static $operations = [ |
|||
'add', |
|||
'subtract', |
|||
'multiply', |
|||
'divideby', |
|||
'divideinto', |
|||
'directsum', |
|||
]; |
|||
|
|||
/** |
|||
* Returns the result of the function call or operation |
|||
* |
|||
* @param string $functionName |
|||
* @param mixed[] $arguments |
|||
* @return Matrix|float |
|||
* @throws Exception |
|||
*/ |
|||
public function __call(string $functionName, $arguments) |
|||
{ |
|||
$functionName = strtolower(str_replace('_', '', $functionName)); |
|||
|
|||
// Test for function calls |
|||
if (in_array($functionName, self::$functions, true)) { |
|||
return Functions::$functionName($this, ...$arguments); |
|||
} |
|||
// Test for operation calls |
|||
if (in_array($functionName, self::$operations, true)) { |
|||
return Operations::$functionName($this, ...$arguments); |
|||
} |
|||
throw new Exception('Function or Operation does not exist'); |
|||
} |
|||
} |
|||
@ -0,0 +1,157 @@ |
|||
<?php |
|||
|
|||
namespace Matrix; |
|||
|
|||
use Matrix\Operators\Addition; |
|||
use Matrix\Operators\DirectSum; |
|||
use Matrix\Operators\Division; |
|||
use Matrix\Operators\Multiplication; |
|||
use Matrix\Operators\Subtraction; |
|||
|
|||
class Operations |
|||
{ |
|||
public static function add(...$matrixValues): Matrix |
|||
{ |
|||
if (count($matrixValues) < 2) { |
|||
throw new Exception('Addition operation requires at least 2 arguments'); |
|||
} |
|||
|
|||
$matrix = array_shift($matrixValues); |
|||
|
|||
if (is_array($matrix)) { |
|||
$matrix = new Matrix($matrix); |
|||
} |
|||
if (!$matrix instanceof Matrix) { |
|||
throw new Exception('Addition arguments must be Matrix or array'); |
|||
} |
|||
|
|||
$result = new Addition($matrix); |
|||
|
|||
foreach ($matrixValues as $matrix) { |
|||
$result->execute($matrix); |
|||
} |
|||
|
|||
return $result->result(); |
|||
} |
|||
|
|||
public static function directsum(...$matrixValues): Matrix |
|||
{ |
|||
if (count($matrixValues) < 2) { |
|||
throw new Exception('DirectSum operation requires at least 2 arguments'); |
|||
} |
|||
|
|||
$matrix = array_shift($matrixValues); |
|||
|
|||
if (is_array($matrix)) { |
|||
$matrix = new Matrix($matrix); |
|||
} |
|||
if (!$matrix instanceof Matrix) { |
|||
throw new Exception('DirectSum arguments must be Matrix or array'); |
|||
} |
|||
|
|||
$result = new DirectSum($matrix); |
|||
|
|||
foreach ($matrixValues as $matrix) { |
|||
$result->execute($matrix); |
|||
} |
|||
|
|||
return $result->result(); |
|||
} |
|||
|
|||
public static function divideby(...$matrixValues): Matrix |
|||
{ |
|||
if (count($matrixValues) < 2) { |
|||
throw new Exception('Division operation requires at least 2 arguments'); |
|||
} |
|||
|
|||
$matrix = array_shift($matrixValues); |
|||
|
|||
if (is_array($matrix)) { |
|||
$matrix = new Matrix($matrix); |
|||
} |
|||
if (!$matrix instanceof Matrix) { |
|||
throw new Exception('Division arguments must be Matrix or array'); |
|||
} |
|||
|
|||
$result = new Division($matrix); |
|||
|
|||
foreach ($matrixValues as $matrix) { |
|||
$result->execute($matrix); |
|||
} |
|||
|
|||
return $result->result(); |
|||
} |
|||
|
|||
public static function divideinto(...$matrixValues): Matrix |
|||
{ |
|||
if (count($matrixValues) < 2) { |
|||
throw new Exception('Division operation requires at least 2 arguments'); |
|||
} |
|||
|
|||
$matrix = array_pop($matrixValues); |
|||
$matrixValues = array_reverse($matrixValues); |
|||
|
|||
if (is_array($matrix)) { |
|||
$matrix = new Matrix($matrix); |
|||
} |
|||
if (!$matrix instanceof Matrix) { |
|||
throw new Exception('Division arguments must be Matrix or array'); |
|||
} |
|||
|
|||
$result = new Division($matrix); |
|||
|
|||
foreach ($matrixValues as $matrix) { |
|||
$result->execute($matrix); |
|||
} |
|||
|
|||
return $result->result(); |
|||
} |
|||
|
|||
public static function multiply(...$matrixValues): Matrix |
|||
{ |
|||
if (count($matrixValues) < 2) { |
|||
throw new Exception('Multiplication operation requires at least 2 arguments'); |
|||
} |
|||
|
|||
$matrix = array_shift($matrixValues); |
|||
|
|||
if (is_array($matrix)) { |
|||
$matrix = new Matrix($matrix); |
|||
} |
|||
if (!$matrix instanceof Matrix) { |
|||
throw new Exception('Multiplication arguments must be Matrix or array'); |
|||
} |
|||
|
|||
$result = new Multiplication($matrix); |
|||
|
|||
foreach ($matrixValues as $matrix) { |
|||
$result->execute($matrix); |
|||
} |
|||
|
|||
return $result->result(); |
|||
} |
|||
|
|||
public static function subtract(...$matrixValues): Matrix |
|||
{ |
|||
if (count($matrixValues) < 2) { |
|||
throw new Exception('Subtraction operation requires at least 2 arguments'); |
|||
} |
|||
|
|||
$matrix = array_shift($matrixValues); |
|||
|
|||
if (is_array($matrix)) { |
|||
$matrix = new Matrix($matrix); |
|||
} |
|||
if (!$matrix instanceof Matrix) { |
|||
throw new Exception('Subtraction arguments must be Matrix or array'); |
|||
} |
|||
|
|||
$result = new Subtraction($matrix); |
|||
|
|||
foreach ($matrixValues as $matrix) { |
|||
$result->execute($matrix); |
|||
} |
|||
|
|||
return $result->result(); |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Operators; |
|||
|
|||
use Matrix\Matrix; |
|||
use Matrix\Exception; |
|||
|
|||
class Addition extends Operator |
|||
{ |
|||
/** |
|||
* Execute the addition |
|||
* |
|||
* @param mixed $value The matrix or numeric value to add to the current base value |
|||
* @throws Exception If the provided argument is not appropriate for the operation |
|||
* @return $this The operation object, allowing multiple additions to be chained |
|||
**/ |
|||
public function execute($value): Operator |
|||
{ |
|||
if (is_array($value)) { |
|||
$value = new Matrix($value); |
|||
} |
|||
|
|||
if (is_object($value) && ($value instanceof Matrix)) { |
|||
return $this->addMatrix($value); |
|||
} elseif (is_numeric($value)) { |
|||
return $this->addScalar($value); |
|||
} |
|||
|
|||
throw new Exception('Invalid argument for addition'); |
|||
} |
|||
|
|||
/** |
|||
* Execute the addition for a scalar |
|||
* |
|||
* @param mixed $value The numeric value to add to the current base value |
|||
* @return $this The operation object, allowing multiple additions to be chained |
|||
**/ |
|||
protected function addScalar($value): Operator |
|||
{ |
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
$this->matrix[$row][$column] += $value; |
|||
} |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Execute the addition for a matrix |
|||
* |
|||
* @param Matrix $value The numeric value to add to the current base value |
|||
* @return $this The operation object, allowing multiple additions to be chained |
|||
* @throws Exception If the provided argument is not appropriate for the operation |
|||
**/ |
|||
protected function addMatrix(Matrix $value): Operator |
|||
{ |
|||
$this->validateMatchingDimensions($value); |
|||
|
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
$this->matrix[$row][$column] += $value->getValue($row + 1, $column + 1); |
|||
} |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,64 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Operators; |
|||
|
|||
use Matrix\Matrix; |
|||
use Matrix\Exception; |
|||
|
|||
class DirectSum extends Operator |
|||
{ |
|||
/** |
|||
* Execute the addition |
|||
* |
|||
* @param mixed $value The matrix or numeric value to add to the current base value |
|||
* @return $this The operation object, allowing multiple additions to be chained |
|||
* @throws Exception If the provided argument is not appropriate for the operation |
|||
*/ |
|||
public function execute($value): Operator |
|||
{ |
|||
if (is_array($value)) { |
|||
$value = new Matrix($value); |
|||
} |
|||
|
|||
if ($value instanceof Matrix) { |
|||
return $this->directSumMatrix($value); |
|||
} |
|||
|
|||
throw new Exception('Invalid argument for addition'); |
|||
} |
|||
|
|||
/** |
|||
* Execute the direct sum for a matrix |
|||
* |
|||
* @param Matrix $value The numeric value to concatenate/direct sum with the current base value |
|||
* @return $this The operation object, allowing multiple additions to be chained |
|||
**/ |
|||
private function directSumMatrix($value): Operator |
|||
{ |
|||
$originalColumnCount = count($this->matrix[0]); |
|||
$originalRowCount = count($this->matrix); |
|||
$valColumnCount = $value->columns; |
|||
$valRowCount = $value->rows; |
|||
$value = $value->toArray(); |
|||
|
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
$this->matrix[$row] = array_merge($this->matrix[$row], array_fill(0, $valColumnCount, 0)); |
|||
} |
|||
|
|||
$this->matrix = array_merge( |
|||
$this->matrix, |
|||
array_fill(0, $valRowCount, array_fill(0, $originalColumnCount, 0)) |
|||
); |
|||
|
|||
for ($row = $originalRowCount; $row < $originalRowCount + $valRowCount; ++$row) { |
|||
array_splice( |
|||
$this->matrix[$row], |
|||
$originalColumnCount, |
|||
$valColumnCount, |
|||
$value[$row - $originalRowCount] |
|||
); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Operators; |
|||
|
|||
use Matrix\Div0Exception; |
|||
use Matrix\Exception; |
|||
use \Matrix\Matrix; |
|||
use \Matrix\Functions; |
|||
|
|||
class Division extends Multiplication |
|||
{ |
|||
/** |
|||
* Execute the division |
|||
* |
|||
* @param mixed $value The matrix or numeric value to divide the current base value by |
|||
* @throws Exception If the provided argument is not appropriate for the operation |
|||
* @return $this The operation object, allowing multiple divisions to be chained |
|||
**/ |
|||
public function execute($value, string $type = 'division'): Operator |
|||
{ |
|||
if (is_array($value)) { |
|||
$value = new Matrix($value); |
|||
} |
|||
|
|||
if (is_object($value) && ($value instanceof Matrix)) { |
|||
$value = Functions::inverse($value, $type); |
|||
|
|||
return $this->multiplyMatrix($value, $type); |
|||
} elseif (is_numeric($value)) { |
|||
return $this->multiplyScalar(1 / $value, $type); |
|||
} |
|||
|
|||
throw new Exception('Invalid argument for division'); |
|||
} |
|||
} |
|||
@ -0,0 +1,86 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Operators; |
|||
|
|||
use Matrix\Matrix; |
|||
use \Matrix\Builder; |
|||
use Matrix\Exception; |
|||
use Throwable; |
|||
|
|||
class Multiplication extends Operator |
|||
{ |
|||
/** |
|||
* Execute the multiplication |
|||
* |
|||
* @param mixed $value The matrix or numeric value to multiply the current base value by |
|||
* @throws Exception If the provided argument is not appropriate for the operation |
|||
* @return $this The operation object, allowing multiple multiplications to be chained |
|||
**/ |
|||
public function execute($value, string $type = 'multiplication'): Operator |
|||
{ |
|||
if (is_array($value)) { |
|||
$value = new Matrix($value); |
|||
} |
|||
|
|||
if (is_object($value) && ($value instanceof Matrix)) { |
|||
return $this->multiplyMatrix($value, $type); |
|||
} elseif (is_numeric($value)) { |
|||
return $this->multiplyScalar($value, $type); |
|||
} |
|||
|
|||
throw new Exception("Invalid argument for $type"); |
|||
} |
|||
|
|||
/** |
|||
* Execute the multiplication for a scalar |
|||
* |
|||
* @param mixed $value The numeric value to multiply with the current base value |
|||
* @return $this The operation object, allowing multiple mutiplications to be chained |
|||
**/ |
|||
protected function multiplyScalar($value, string $type = 'multiplication'): Operator |
|||
{ |
|||
try { |
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
$this->matrix[$row][$column] *= $value; |
|||
} |
|||
} |
|||
} catch (Throwable $e) { |
|||
throw new Exception("Invalid argument for $type"); |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Execute the multiplication for a matrix |
|||
* |
|||
* @param Matrix $value The numeric value to multiply with the current base value |
|||
* @return $this The operation object, allowing multiple mutiplications to be chained |
|||
* @throws Exception If the provided argument is not appropriate for the operation |
|||
**/ |
|||
protected function multiplyMatrix(Matrix $value, string $type = 'multiplication'): Operator |
|||
{ |
|||
$this->validateReflectingDimensions($value); |
|||
|
|||
$newRows = $this->rows; |
|||
$newColumns = $value->columns; |
|||
$matrix = Builder::createFilledMatrix(0, $newRows, $newColumns) |
|||
->toArray(); |
|||
try { |
|||
for ($row = 0; $row < $newRows; ++$row) { |
|||
for ($column = 0; $column < $newColumns; ++$column) { |
|||
$columnData = $value->getColumns($column + 1)->toArray(); |
|||
foreach ($this->matrix[$row] as $key => $valueData) { |
|||
$matrix[$row][$column] += $valueData * $columnData[$key][0]; |
|||
} |
|||
} |
|||
} |
|||
} catch (Throwable $e) { |
|||
throw new Exception("Invalid argument for $type"); |
|||
} |
|||
$this->matrix = $matrix; |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,78 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Operators; |
|||
|
|||
use Matrix\Matrix; |
|||
use Matrix\Exception; |
|||
|
|||
abstract class Operator |
|||
{ |
|||
/** |
|||
* Stored internally as a 2-dimension array of values |
|||
* |
|||
* @property mixed[][] $matrix |
|||
**/ |
|||
protected $matrix; |
|||
|
|||
/** |
|||
* Number of rows in the matrix |
|||
* |
|||
* @property integer $rows |
|||
**/ |
|||
protected $rows; |
|||
|
|||
/** |
|||
* Number of columns in the matrix |
|||
* |
|||
* @property integer $columns |
|||
**/ |
|||
protected $columns; |
|||
|
|||
/** |
|||
* Create an new handler object for the operation |
|||
* |
|||
* @param Matrix $matrix The base Matrix object on which the operation will be performed |
|||
*/ |
|||
public function __construct(Matrix $matrix) |
|||
{ |
|||
$this->rows = $matrix->rows; |
|||
$this->columns = $matrix->columns; |
|||
$this->matrix = $matrix->toArray(); |
|||
} |
|||
|
|||
/** |
|||
* Compare the dimensions of the matrices being operated on to see if they are valid for addition/subtraction |
|||
* |
|||
* @param Matrix $matrix The second Matrix object on which the operation will be performed |
|||
* @throws Exception |
|||
*/ |
|||
protected function validateMatchingDimensions(Matrix $matrix): void |
|||
{ |
|||
if (($this->rows != $matrix->rows) || ($this->columns != $matrix->columns)) { |
|||
throw new Exception('Matrices have mismatched dimensions'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Compare the dimensions of the matrices being operated on to see if they are valid for multiplication/division |
|||
* |
|||
* @param Matrix $matrix The second Matrix object on which the operation will be performed |
|||
* @throws Exception |
|||
*/ |
|||
protected function validateReflectingDimensions(Matrix $matrix): void |
|||
{ |
|||
if ($this->columns != $matrix->rows) { |
|||
throw new Exception('Matrices have mismatched dimensions'); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Return the result of the operation |
|||
* |
|||
* @return Matrix |
|||
*/ |
|||
public function result(): Matrix |
|||
{ |
|||
return new Matrix($this->matrix); |
|||
} |
|||
} |
|||
@ -0,0 +1,68 @@ |
|||
<?php |
|||
|
|||
namespace Matrix\Operators; |
|||
|
|||
use Matrix\Matrix; |
|||
use Matrix\Exception; |
|||
|
|||
class Subtraction extends Operator |
|||
{ |
|||
/** |
|||
* Execute the subtraction |
|||
* |
|||
* @param mixed $value The matrix or numeric value to subtract from the current base value |
|||
* @throws Exception If the provided argument is not appropriate for the operation |
|||
* @return $this The operation object, allowing multiple subtractions to be chained |
|||
**/ |
|||
public function execute($value): Operator |
|||
{ |
|||
if (is_array($value)) { |
|||
$value = new Matrix($value); |
|||
} |
|||
|
|||
if (is_object($value) && ($value instanceof Matrix)) { |
|||
return $this->subtractMatrix($value); |
|||
} elseif (is_numeric($value)) { |
|||
return $this->subtractScalar($value); |
|||
} |
|||
|
|||
throw new Exception('Invalid argument for subtraction'); |
|||
} |
|||
|
|||
/** |
|||
* Execute the subtraction for a scalar |
|||
* |
|||
* @param mixed $value The numeric value to subtracted from the current base value |
|||
* @return $this The operation object, allowing multiple additions to be chained |
|||
**/ |
|||
protected function subtractScalar($value): Operator |
|||
{ |
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
$this->matrix[$row][$column] -= $value; |
|||
} |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* Execute the subtraction for a matrix |
|||
* |
|||
* @param Matrix $value The numeric value to subtract from the current base value |
|||
* @return $this The operation object, allowing multiple subtractions to be chained |
|||
* @throws Exception If the provided argument is not appropriate for the operation |
|||
**/ |
|||
protected function subtractMatrix(Matrix $value): Operator |
|||
{ |
|||
$this->validateMatchingDimensions($value); |
|||
|
|||
for ($row = 0; $row < $this->rows; ++$row) { |
|||
for ($column = 0; $column < $this->columns; ++$column) { |
|||
$this->matrix[$row][$column] -= $value->getValue($row + 1, $column + 1); |
|||
} |
|||
} |
|||
|
|||
return $this; |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
{ |
|||
"name": "markbaker/matrix", |
|||
"type": "library", |
|||
"description": "PHP Class for working with matrices", |
|||
"keywords": ["matrix", "vector", "mathematics"], |
|||
"homepage": "https://github.com/MarkBaker/PHPMatrix", |
|||
"license": "MIT", |
|||
"authors": [ |
|||
{ |
|||
"name": "Mark Baker", |
|||
"email": "mark@demon-angel.eu" |
|||
} |
|||
], |
|||
"require": { |
|||
"php": "^7.1 || ^8.0" |
|||
}, |
|||
"require-dev": { |
|||
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.3", |
|||
"phpdocumentor/phpdocumentor": "2.*", |
|||
"phpmd/phpmd": "2.*", |
|||
"sebastian/phpcpd": "^4.0", |
|||
"phploc/phploc": "^4.0", |
|||
"squizlabs/php_codesniffer": "^3.4", |
|||
"phpcompatibility/php-compatibility": "^9.0", |
|||
"dealerdirect/phpcodesniffer-composer-installer": "^0.7.0" |
|||
}, |
|||
"autoload": { |
|||
"psr-4": { |
|||
"Matrix\\": "classes/src/" |
|||
} |
|||
}, |
|||
"autoload-dev": { |
|||
"psr-4": { |
|||
"MatrixTest\\": "unitTests/classes/src/" |
|||
} |
|||
}, |
|||
"scripts": { |
|||
"style": "phpcs --report-width=200 --standard=PSR2 --report=summary,full classes/src/ unitTests/classes/src -n", |
|||
"test": "phpunit -c phpunit.xml.dist", |
|||
"mess": "phpmd classes/src/ xml codesize,unusedcode,design,naming -n", |
|||
"lines": "phploc classes/src/ -n", |
|||
"cpd": "phpcpd classes/src/ -n", |
|||
"versions": "phpcs --report-width=200 --standard=PHPCompatibility --report=summary,full classes/src/ --runtime-set testVersion 7.2- -n", |
|||
"coverage": "phpunit -c phpunit.xml.dist --coverage-text --coverage-html ./build/coverage" |
|||
}, |
|||
"minimum-stability": "dev" |
|||
} |
|||
@ -0,0 +1,33 @@ |
|||
<?php |
|||
|
|||
use Matrix\Matrix; |
|||
use Matrix\Decomposition\QR; |
|||
|
|||
include __DIR__ . '/../vendor/autoload.php'; |
|||
|
|||
$grid = [ |
|||
[0, 1], |
|||
[-1, 0], |
|||
]; |
|||
|
|||
$targetGrid = [ |
|||
[-1], |
|||
[2], |
|||
]; |
|||
|
|||
$matrix = new Matrix($grid); |
|||
$target = new Matrix($targetGrid); |
|||
|
|||
$decomposition = new QR($matrix); |
|||
|
|||
$X = $decomposition->solve($target); |
|||
|
|||
echo 'X', PHP_EOL; |
|||
var_export($X->toArray()); |
|||
echo PHP_EOL; |
|||
|
|||
$resolve = $matrix->multiply($X); |
|||
|
|||
echo 'Resolve', PHP_EOL; |
|||
var_export($resolve->toArray()); |
|||
echo PHP_EOL; |
|||
@ -0,0 +1,17 @@ |
|||
{ |
|||
"timeout": 1, |
|||
"source": { |
|||
"directories": [ |
|||
"classes\/src" |
|||
] |
|||
}, |
|||
"logs": { |
|||
"text": "build/infection/text.log", |
|||
"summary": "build/infection/summary.log", |
|||
"debug": "build/infection/debug.log", |
|||
"perMutator": "build/infection/perMutator.md" |
|||
}, |
|||
"mutators": { |
|||
"@default": true |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
The MIT License (MIT) |
|||
===================== |
|||
|
|||
Copyright © `2018` `Mark Baker` |
|||
|
|||
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,6 @@ |
|||
parameters: |
|||
ignoreErrors: |
|||
- '#Property [A-Za-z\\]+::\$[A-Za-z]+ has no typehint specified#' |
|||
- '#Method [A-Za-z\\]+::[A-Za-z]+\(\) has no return typehint specified#' |
|||
- '#Method [A-Za-z\\]+::[A-Za-z]+\(\) has parameter \$[A-Za-z0-9]+ with no typehint specified#' |
|||
checkMissingIterableValueType: false |
|||
@ -0,0 +1,6 @@ |
|||
# Auto detect text files and perform LF normalization |
|||
* text=auto |
|||
|
|||
tests/ export-ignore |
|||
phpunit.xml export-ignore |
|||
.travis.yml export-ignore |
|||
@ -0,0 +1,6 @@ |
|||
.DS_Store |
|||
nbproject/* |
|||
.idea/* |
|||
vendor/* |
|||
composer.phar |
|||
composer.lock |
|||
@ -0,0 +1,18 @@ |
|||
php-enum - PHP Enum implementation http://github.com/myclabs/php-enum |
|||
|
|||
Copyright (C) 2015 My C-Labs |
|||
|
|||
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,128 @@ |
|||
# PHP Enum implementation inspired from SplEnum |
|||
|
|||
[](https://travis-ci.org/myclabs/php-enum) |
|||
[](https://packagist.org/packages/myclabs/php-enum) |
|||
[](https://packagist.org/packages/myclabs/php-enum) |
|||
|
|||
## Why? |
|||
|
|||
First, and mainly, `SplEnum` is not integrated to PHP, you have to install it separately. |
|||
|
|||
Using an enum instead of class constants provides the following advantages: |
|||
|
|||
- You can type-hint: `function setAction(Action $action) {` |
|||
- You can enrich the enum with methods (e.g. `format`, `parse`, …) |
|||
- You can extend the enum to add new values (make your enum `final` to prevent it) |
|||
- You can get a list of all the possible values (see below) |
|||
|
|||
This Enum class is not intended to replace class constants, but only to be used when it makes sense. |
|||
|
|||
## Installation |
|||
|
|||
``` |
|||
composer require myclabs/php-enum |
|||
``` |
|||
|
|||
## Declaration |
|||
|
|||
```php |
|||
use MyCLabs\Enum\Enum; |
|||
|
|||
/** |
|||
* Action enum |
|||
*/ |
|||
class Action extends Enum |
|||
{ |
|||
private const VIEW = 'view'; |
|||
private const EDIT = 'edit'; |
|||
} |
|||
``` |
|||
|
|||
Note the `private` keyword requires PHP > 7.1, you can omit it on PHP 7.0. |
|||
|
|||
## Usage |
|||
|
|||
```php |
|||
$action = new Action(Action::VIEW); |
|||
|
|||
// or |
|||
$action = Action::VIEW(); |
|||
``` |
|||
|
|||
As you can see, static methods are automatically implemented to provide quick access to an enum value. |
|||
|
|||
One advantage over using class constants is to be able to type-hint enum values: |
|||
|
|||
```php |
|||
function setAction(Action $action) { |
|||
// ... |
|||
} |
|||
``` |
|||
|
|||
## Documentation |
|||
|
|||
- `__construct()` The constructor checks that the value exist in the enum |
|||
- `__toString()` You can `echo $myValue`, it will display the enum value (value of the constant) |
|||
- `getValue()` Returns the current value of the enum |
|||
- `getKey()` Returns the key of the current value on Enum |
|||
- `equals()` Tests whether enum instances are equal (returns `true` if enum values are equal, `false` otherwise) |
|||
|
|||
Static methods: |
|||
|
|||
- `toArray()` method Returns all possible values as an array (constant name in key, constant value in value) |
|||
- `keys()` Returns the names (keys) of all constants in the Enum class |
|||
- `values()` Returns instances of the Enum class of all Enum constants (constant name in key, Enum instance in value) |
|||
- `isValid()` Check if tested value is valid on enum set |
|||
- `isValidKey()` Check if tested key is valid on enum set |
|||
- `search()` Return key for searched value |
|||
|
|||
### Static methods |
|||
|
|||
```php |
|||
class Action extends Enum |
|||
{ |
|||
private const VIEW = 'view'; |
|||
private const EDIT = 'edit'; |
|||
} |
|||
|
|||
// Static method: |
|||
$action = Action::VIEW(); |
|||
$action = Action::EDIT(); |
|||
``` |
|||
|
|||
Static method helpers are implemented using [`__callStatic()`](http://www.php.net/manual/en/language.oop5.overloading.php#object.callstatic). |
|||
|
|||
If you care about IDE autocompletion, you can either implement the static methods yourself: |
|||
|
|||
```php |
|||
class Action extends Enum |
|||
{ |
|||
private const VIEW = 'view'; |
|||
|
|||
/** |
|||
* @return Action |
|||
*/ |
|||
public static function VIEW() { |
|||
return new Action(self::VIEW); |
|||
} |
|||
} |
|||
``` |
|||
|
|||
or you can use phpdoc (this is supported in PhpStorm for example): |
|||
|
|||
```php |
|||
/** |
|||
* @method static Action VIEW() |
|||
* @method static Action EDIT() |
|||
*/ |
|||
class Action extends Enum |
|||
{ |
|||
private const VIEW = 'view'; |
|||
private const EDIT = 'edit'; |
|||
} |
|||
``` |
|||
|
|||
## Related projects |
|||
|
|||
- [Doctrine enum mapping](https://github.com/acelaya/doctrine-enum-type) |
|||
- [Symfony 2/3 ParamConverter integration](https://github.com/Ex3v/MyCLabsEnumParamConverter) |
|||
@ -0,0 +1,32 @@ |
|||
{ |
|||
"name": "myclabs/php-enum", |
|||
"type": "library", |
|||
"description": "PHP Enum implementation", |
|||
"keywords": ["enum"], |
|||
"homepage": "http://github.com/myclabs/php-enum", |
|||
"license": "MIT", |
|||
"authors": [ |
|||
{ |
|||
"name": "PHP Enum contributors", |
|||
"homepage": "https://github.com/myclabs/php-enum/graphs/contributors" |
|||
} |
|||
], |
|||
"autoload": { |
|||
"psr-4": { |
|||
"MyCLabs\\Enum\\": "src/" |
|||
} |
|||
}, |
|||
"autoload-dev": { |
|||
"psr-4": { |
|||
"MyCLabs\\Tests\\Enum\\": "tests/" |
|||
} |
|||
}, |
|||
"require": { |
|||
"php": ">=5.4", |
|||
"ext-json": "*" |
|||
}, |
|||
"require-dev": { |
|||
"phpunit/phpunit": "^4.8.35|^5.7|^6.0", |
|||
"squizlabs/php_codesniffer": "1.*" |
|||
} |
|||
} |
|||
@ -0,0 +1,205 @@ |
|||
<?php |
|||
/** |
|||
* @link http://github.com/myclabs/php-enum |
|||
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file) |
|||
*/ |
|||
|
|||
namespace MyCLabs\Enum; |
|||
|
|||
/** |
|||
* Base Enum class |
|||
* |
|||
* Create an enum by implementing this class and adding class constants. |
|||
* |
|||
* @author Matthieu Napoli <matthieu@mnapoli.fr> |
|||
* @author Daniel Costa <danielcosta@gmail.com> |
|||
* @author Mirosław Filip <mirfilip@gmail.com> |
|||
*/ |
|||
abstract class Enum implements \JsonSerializable |
|||
{ |
|||
/** |
|||
* Enum value |
|||
* |
|||
* @var mixed |
|||
*/ |
|||
protected $value; |
|||
|
|||
/** |
|||
* Store existing constants in a static cache per object. |
|||
* |
|||
* @var array |
|||
*/ |
|||
protected static $cache = []; |
|||
|
|||
/** |
|||
* Creates a new value of some type |
|||
* |
|||
* @param mixed $value |
|||
* |
|||
* @throws \UnexpectedValueException if incompatible type is given. |
|||
*/ |
|||
public function __construct($value) |
|||
{ |
|||
if ($value instanceof static) { |
|||
$this->value = $value->getValue(); |
|||
|
|||
return; |
|||
} |
|||
|
|||
if (!$this->isValid($value)) { |
|||
throw new \UnexpectedValueException("Value '$value' is not part of the enum " . \get_called_class()); |
|||
} |
|||
|
|||
$this->value = $value; |
|||
} |
|||
|
|||
/** |
|||
* @return mixed |
|||
*/ |
|||
public function getValue() |
|||
{ |
|||
return $this->value; |
|||
} |
|||
|
|||
/** |
|||
* Returns the enum key (i.e. the constant name). |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public function getKey() |
|||
{ |
|||
return static::search($this->value); |
|||
} |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function __toString() |
|||
{ |
|||
return (string)$this->value; |
|||
} |
|||
|
|||
/** |
|||
* Compares one Enum with another. |
|||
* |
|||
* This method is final, for more information read https://github.com/myclabs/php-enum/issues/4 |
|||
* |
|||
* @return bool True if Enums are equal, false if not equal |
|||
*/ |
|||
final public function equals(Enum $enum = null) |
|||
{ |
|||
return $enum !== null && $this->getValue() === $enum->getValue() && \get_called_class() === \get_class($enum); |
|||
} |
|||
|
|||
/** |
|||
* Returns the names (keys) of all constants in the Enum class |
|||
* |
|||
* @return array |
|||
*/ |
|||
public static function keys() |
|||
{ |
|||
return \array_keys(static::toArray()); |
|||
} |
|||
|
|||
/** |
|||
* Returns instances of the Enum class of all Enum constants |
|||
* |
|||
* @return static[] Constant name in key, Enum instance in value |
|||
*/ |
|||
public static function values() |
|||
{ |
|||
$values = array(); |
|||
|
|||
foreach (static::toArray() as $key => $value) { |
|||
$values[$key] = new static($value); |
|||
} |
|||
|
|||
return $values; |
|||
} |
|||
|
|||
/** |
|||
* Returns all possible values as an array |
|||
* |
|||
* @return array Constant name in key, constant value in value |
|||
*/ |
|||
public static function toArray() |
|||
{ |
|||
$class = \get_called_class(); |
|||
if (!isset(static::$cache[$class])) { |
|||
$reflection = new \ReflectionClass($class); |
|||
static::$cache[$class] = $reflection->getConstants(); |
|||
} |
|||
|
|||
return static::$cache[$class]; |
|||
} |
|||
|
|||
/** |
|||
* Check if is valid enum value |
|||
* |
|||
* @param $value |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public static function isValid($value) |
|||
{ |
|||
return \in_array($value, static::toArray(), true); |
|||
} |
|||
|
|||
/** |
|||
* Check if is valid enum key |
|||
* |
|||
* @param $key |
|||
* |
|||
* @return bool |
|||
*/ |
|||
public static function isValidKey($key) |
|||
{ |
|||
$array = static::toArray(); |
|||
|
|||
return isset($array[$key]) || \array_key_exists($key, $array); |
|||
} |
|||
|
|||
/** |
|||
* Return key for value |
|||
* |
|||
* @param $value |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
public static function search($value) |
|||
{ |
|||
return \array_search($value, static::toArray(), true); |
|||
} |
|||
|
|||
/** |
|||
* Returns a value when called statically like so: MyEnum::SOME_VALUE() given SOME_VALUE is a class constant |
|||
* |
|||
* @param string $name |
|||
* @param array $arguments |
|||
* |
|||
* @return static |
|||
* @throws \BadMethodCallException |
|||
*/ |
|||
public static function __callStatic($name, $arguments) |
|||
{ |
|||
$array = static::toArray(); |
|||
if (isset($array[$name]) || \array_key_exists($name, $array)) { |
|||
return new static($array[$name]); |
|||
} |
|||
|
|||
throw new \BadMethodCallException("No static method or enum constant '$name' in class " . \get_called_class()); |
|||
} |
|||
|
|||
/** |
|||
* Specify data which should be serialized to JSON. This method returns data that can be serialized by json_encode() |
|||
* natively. |
|||
* |
|||
* @return mixed |
|||
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php |
|||
*/ |
|||
#[\ReturnTypeWillChange] |
|||
public function jsonSerialize() |
|||
{ |
|||
return $this->getValue(); |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
root = true |
|||
|
|||
[*] |
|||
indent_style = space |
|||
indent_size = 4 |
|||
end_of_line = lf |
|||
charset = utf-8 |
|||
trim_trailing_whitespace = true |
|||
insert_final_newline = false |
|||
|
|||
[*.{vue,js,scss}] |
|||
charset = utf-8 |
|||
indent_style = space |
|||
indent_size = 2 |
|||
end_of_line = lf |
|||
insert_final_newline = true |
|||
trim_trailing_whitespace = true |
|||
|
|||
[*.md] |
|||
trim_trailing_whitespace = false |
|||
@ -0,0 +1,3 @@ |
|||
# These are supported funding model platforms |
|||
|
|||
github: [overtrue] |
|||
@ -0,0 +1,24 @@ |
|||
name: Tests |
|||
|
|||
on: |
|||
push: |
|||
branches: [ master ] |
|||
pull_request: |
|||
|
|||
jobs: |
|||
phpunit: |
|||
strategy: |
|||
matrix: |
|||
php_version: [5.6, 7.0, 7.1, 7.2, 7.3, 7.4, 8.0, 8.1] |
|||
runs-on: ubuntu-latest |
|||
steps: |
|||
- uses: actions/checkout@v2 |
|||
- name: Setup PHP environment |
|||
uses: shivammathur/setup-php@v2 |
|||
with: |
|||
php-version: ${{ matrix.php_version }} |
|||
coverage: xdebug |
|||
- name: Install dependencies |
|||
run: composer install |
|||
- name: PHPUnit check |
|||
run: ./vendor/bin/phpunit --coverage-text |
|||
@ -0,0 +1,49 @@ |
|||
<?php |
|||
|
|||
return (new PhpCsFixer\Config()) |
|||
->setRules([ |
|||
'@PSR12' => true, |
|||
'binary_operator_spaces' => true, |
|||
'blank_line_after_opening_tag' => true, |
|||
'compact_nullable_typehint' => true, |
|||
'declare_equal_normalize' => true, |
|||
'lowercase_cast' => true, |
|||
'lowercase_static_reference' => true, |
|||
'new_with_braces' => true, |
|||
'no_blank_lines_after_class_opening' => true, |
|||
'no_leading_import_slash' => true, |
|||
'no_whitespace_in_blank_line' => true, |
|||
'no_unused_imports' => true, |
|||
'ordered_class_elements' => [ |
|||
'order' => [ |
|||
'use_trait', |
|||
], |
|||
], |
|||
'ordered_imports' => [ |
|||
'imports_order' => [ |
|||
'class', |
|||
'function', |
|||
'const', |
|||
], |
|||
'sort_algorithm' => 'none', |
|||
], |
|||
'return_type_declaration' => true, |
|||
'short_scalar_cast' => true, |
|||
'single_blank_line_before_namespace' => true, |
|||
'single_trait_insert_per_statement' => true, |
|||
'ternary_operator_spaces' => true, |
|||
'unary_operator_spaces' => true, |
|||
'visibility_required' => [ |
|||
'elements' => [ |
|||
// 'const', |
|||
'method', |
|||
'property', |
|||
], |
|||
], |
|||
]) |
|||
->setFinder( |
|||
PhpCsFixer\Finder::create() |
|||
->exclude('vendor') |
|||
->in([__DIR__.'/src/', __DIR__.'/tests/']) |
|||
) |
|||
; |
|||
@ -0,0 +1,884 @@ |
|||
<h1 align="center">Easy SMS</h1> |
|||
|
|||
<p align="center">:calling: 一款满足你的多种发送需求的短信发送组件</p> |
|||
|
|||
<p align="center"> |
|||
<a href="https://travis-ci.org/overtrue/easy-sms"><img src="https://travis-ci.org/overtrue/easy-sms.svg?branch=master" alt="Build Status"></a> |
|||
<a href="https://packagist.org/packages/overtrue/easy-sms"><img src="https://poser.pugx.org/overtrue/easy-sms/v/stable.svg" alt="Latest Stable Version"></a> |
|||
<a href="https://packagist.org/packages/overtrue/easy-sms"><img src="https://poser.pugx.org/overtrue/easy-sms/v/unstable.svg" alt="Latest Unstable Version"></a> |
|||
<a href="https://scrutinizer-ci.com/g/overtrue/easy-sms/?branch=master"><img src="https://scrutinizer-ci.com/g/overtrue/easy-sms/badges/coverage.png?b=master" alt="Code Coverage"></a> |
|||
<a href="https://packagist.org/packages/overtrue/easy-sms"><img src="https://poser.pugx.org/overtrue/easy-sms/downloads" alt="Total Downloads"></a> |
|||
<a href="https://packagist.org/packages/overtrue/easy-sms"><img src="https://poser.pugx.org/overtrue/easy-sms/license" alt="License"></a> |
|||
</p> |
|||
|
|||
<p align="center"> |
|||
<a href="https://github.com/sponsors/overtrue"><img src="https://github.com/overtrue/overtrue/blob/master/sponsor-me-button-s.svg?raw=true" alt="Sponsor me" style="max-width: 100%;"></a> |
|||
</p> |
|||
|
|||
|
|||
## 特点 |
|||
|
|||
1. 支持目前市面多家服务商 |
|||
1. 一套写法兼容所有平台 |
|||
1. 简单配置即可灵活增减服务商 |
|||
1. 内置多种服务商轮询策略、支持自定义轮询策略 |
|||
1. 统一的返回值格式,便于日志与监控 |
|||
1. 自动轮询选择可用的服务商 |
|||
1. 更多等你去发现与改进... |
|||
|
|||
## 平台支持 |
|||
|
|||
- [腾讯云 SMS](https://cloud.tencent.com/product/sms) |
|||
- [Ucloud](https://www.ucloud.cn) |
|||
- [七牛云](https://www.qiniu.com/) |
|||
- [SendCloud](http://www.sendcloud.net/) |
|||
- [阿里云](https://www.aliyun.com/) |
|||
- [云片](https://www.yunpian.com) |
|||
- [Submail](https://www.mysubmail.com) |
|||
- [螺丝帽](https://luosimao.com/) |
|||
- [容联云通讯](http://www.yuntongxun.com) |
|||
- [互亿无线](http://www.ihuyi.com) |
|||
- [聚合数据](https://www.juhe.cn) |
|||
- [百度云](https://cloud.baidu.com/) |
|||
- [华信短信平台](http://www.ipyy.com/) |
|||
- [253云通讯(创蓝)](https://www.253.com/) |
|||
- [融云](http://www.rongcloud.cn) |
|||
- [天毅无线](http://www.85hu.com/) |
|||
- [阿凡达数据](http://www.avatardata.cn/) |
|||
- [华为云](https://www.huaweicloud.com/product/msgsms.html) |
|||
- [网易云信](https://yunxin.163.com/sms) |
|||
- [云之讯](https://www.ucpaas.com/index.html) |
|||
- [凯信通](http://www.kingtto.cn/) |
|||
- [UE35.net](http://uesms.ue35.cn/) |
|||
- [短信宝](http://www.smsbao.com/) |
|||
- [Tiniyo](https://tiniyo.com/) |
|||
- [摩杜云](https://www.moduyun.com/) |
|||
- [融合云(助通)](https://www.ztinfo.cn/products/sms) |
|||
- [蜘蛛云](https://zzyun.com/) |
|||
- [融合云信](https://maap.wo.cn/) |
|||
|
|||
## 环境需求 |
|||
|
|||
- PHP >= 5.6 |
|||
|
|||
## 安装 |
|||
|
|||
```shell |
|||
$ composer require "overtrue/easy-sms" |
|||
``` |
|||
|
|||
**For Laravel notification** |
|||
|
|||
如果你喜欢使用 [Laravel Notification](https://laravel.com/docs/5.8/notifications), 可以考虑直接使用朋友封装的拓展包: |
|||
|
|||
https://github.com/yl/easysms-notification-channel |
|||
|
|||
## 使用 |
|||
|
|||
```php |
|||
use Overtrue\EasySms\EasySms; |
|||
|
|||
$config = [ |
|||
// HTTP 请求的超时时间(秒) |
|||
'timeout' => 5.0, |
|||
|
|||
// 默认发送配置 |
|||
'default' => [ |
|||
// 网关调用策略,默认:顺序调用 |
|||
'strategy' => \Overtrue\EasySms\Strategies\OrderStrategy::class, |
|||
|
|||
// 默认可用的发送网关 |
|||
'gateways' => [ |
|||
'yunpian', 'aliyun', |
|||
], |
|||
], |
|||
// 可用的网关配置 |
|||
'gateways' => [ |
|||
'errorlog' => [ |
|||
'file' => '/tmp/easy-sms.log', |
|||
], |
|||
'yunpian' => [ |
|||
'api_key' => '824f0ff2f71cab52936axxxxxxxxxx', |
|||
], |
|||
'aliyun' => [ |
|||
'access_key_id' => '', |
|||
'access_key_secret' => '', |
|||
'sign_name' => '', |
|||
], |
|||
//... |
|||
], |
|||
]; |
|||
|
|||
$easySms = new EasySms($config); |
|||
|
|||
$easySms->send(13188888888, [ |
|||
'content' => '您的验证码为: 6379', |
|||
'template' => 'SMS_001', |
|||
'data' => [ |
|||
'code' => 6379 |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
## 短信内容 |
|||
|
|||
由于使用多网关发送,所以一条短信要支持多平台发送,每家的发送方式不一样,但是我们抽象定义了以下公用属性: |
|||
|
|||
- `content` 文字内容,使用在像云片类似的以文字内容发送的平台 |
|||
- `template` 模板 ID,使用在以模板ID来发送短信的平台 |
|||
- `data` 模板变量,使用在以模板ID来发送短信的平台 |
|||
|
|||
所以,在使用过程中你可以根据所要使用的平台定义发送的内容。 |
|||
|
|||
```php |
|||
$easySms->send(13188888888, [ |
|||
'content' => '您的验证码为: 6379', |
|||
'template' => 'SMS_001', |
|||
'data' => [ |
|||
'code' => 6379 |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
你也可以使用闭包来返回对应的值: |
|||
|
|||
```php |
|||
$easySms->send(13188888888, [ |
|||
'content' => function($gateway){ |
|||
return '您的验证码为: 6379'; |
|||
}, |
|||
'template' => function($gateway){ |
|||
return 'SMS_001'; |
|||
}, |
|||
'data' => function($gateway){ |
|||
return [ |
|||
'code' => 6379 |
|||
]; |
|||
}, |
|||
]); |
|||
``` |
|||
|
|||
你可以根据 `$gateway` 参数类型来判断返回值,例如: |
|||
|
|||
```php |
|||
$easySms->send(13188888888, [ |
|||
'content' => function($gateway){ |
|||
if ($gateway->getName() == 'yunpian') { |
|||
return '云片专用验证码:1235'; |
|||
} |
|||
return '您的验证码为: 6379'; |
|||
}, |
|||
'template' => function($gateway){ |
|||
if ($gateway->getName() == 'aliyun') { |
|||
return 'TP2818'; |
|||
} |
|||
return 'SMS_001'; |
|||
}, |
|||
'data' => function($gateway){ |
|||
return [ |
|||
'code' => 6379 |
|||
]; |
|||
}, |
|||
]); |
|||
``` |
|||
|
|||
## 发送网关 |
|||
|
|||
默认使用 `default` 中的设置来发送,如果某一条短信你想要覆盖默认的设置。在 `send` 方法中使用第三个参数即可: |
|||
|
|||
```php |
|||
$easySms->send(13188888888, [ |
|||
'content' => '您的验证码为: 6379', |
|||
'template' => 'SMS_001', |
|||
'data' => [ |
|||
'code' => 6379 |
|||
], |
|||
], ['yunpian', 'juhe']); // 这里的网关配置将会覆盖全局默认值 |
|||
``` |
|||
|
|||
## 返回值 |
|||
|
|||
由于使用多网关发送,所以返回值为一个数组,结构如下: |
|||
```php |
|||
[ |
|||
'yunpian' => [ |
|||
'gateway' => 'yunpian', |
|||
'status' => 'success', |
|||
'result' => [...] // 平台返回值 |
|||
], |
|||
'juhe' => [ |
|||
'gateway' => 'juhe', |
|||
'status' => 'failure', |
|||
'exception' => \Overtrue\EasySms\Exceptions\GatewayErrorException 对象 |
|||
], |
|||
//... |
|||
] |
|||
``` |
|||
|
|||
如果所选网关列表均发送失败时,将会抛出 `Overtrue\EasySms\Exceptions\NoGatewayAvailableException` 异常,你可以使用 `$e->results` 获取发送结果。 |
|||
|
|||
你也可以使用 `$e` 提供的更多便捷方法: |
|||
|
|||
```php |
|||
$e->getResults(); // 返回所有 API 的结果,结构同上 |
|||
$e->getExceptions(); // 返回所有调用异常列表 |
|||
$e->getException($gateway); // 返回指定网关名称的异常对象 |
|||
$e->getLastException(); // 获取最后一个失败的异常对象 |
|||
``` |
|||
|
|||
## 自定义网关 |
|||
|
|||
本拓展已经支持用户自定义网关,你可以很方便的配置即可当成与其它拓展一样的使用: |
|||
|
|||
```php |
|||
$config = [ |
|||
... |
|||
'default' => [ |
|||
'gateways' => [ |
|||
'mygateway', // 配置你的网站到可用的网关列表 |
|||
], |
|||
], |
|||
'gateways' => [ |
|||
'mygateway' => [...], // 你网关所需要的参数,如果没有可以不配置 |
|||
], |
|||
]; |
|||
|
|||
$easySms = new EasySms($config); |
|||
|
|||
// 注册 |
|||
$easySms->extend('mygateway', function($gatewayConfig){ |
|||
// $gatewayConfig 来自配置文件里的 `gateways.mygateway` |
|||
return new MyGateway($gatewayConfig); |
|||
}); |
|||
|
|||
$easySms->send(13188888888, [ |
|||
'content' => '您的验证码为: 6379', |
|||
'template' => 'SMS_001', |
|||
'data' => [ |
|||
'code' => 6379 |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
## 国际短信 |
|||
|
|||
国际短信与国内短信的区别是号码前面需要加国际码,但是由于各平台对国际号码的写法不一致,所以在发送国际短信的时候有一点区别: |
|||
|
|||
```php |
|||
use Overtrue\EasySms\PhoneNumber; |
|||
|
|||
// 发送到国际码为 31 的国际号码 |
|||
$number = new PhoneNumber(13188888888, 31); |
|||
|
|||
$easySms->send($number, [ |
|||
'content' => '您的验证码为: 6379', |
|||
'template' => 'SMS_001', |
|||
'data' => [ |
|||
'code' => 6379 |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
## 定义短信 |
|||
|
|||
你可以根据发送场景的不同,定义不同的短信类,从而实现一处定义多处调用,你可以继承 `Overtrue\EasySms\Message` 来定义短信模型: |
|||
|
|||
```php |
|||
<?php |
|||
|
|||
use Overtrue\EasySms\Message; |
|||
use Overtrue\EasySms\Contracts\GatewayInterface; |
|||
use Overtrue\EasySms\Strategies\OrderStrategy; |
|||
|
|||
class OrderPaidMessage extends Message |
|||
{ |
|||
protected $order; |
|||
protected $strategy = OrderStrategy::class; // 定义本短信的网关使用策略,覆盖全局配置中的 `default.strategy` |
|||
protected $gateways = ['alidayu', 'yunpian', 'juhe']; // 定义本短信的适用平台,覆盖全局配置中的 `default.gateways` |
|||
|
|||
public function __construct($order) |
|||
{ |
|||
$this->order = $order; |
|||
} |
|||
|
|||
// 定义直接使用内容发送平台的内容 |
|||
public function getContent(GatewayInterface $gateway = null) |
|||
{ |
|||
return sprintf('您的订单:%s, 已经完成付款', $this->order->no); |
|||
} |
|||
|
|||
// 定义使用模板发送方式平台所需要的模板 ID |
|||
public function getTemplate(GatewayInterface $gateway = null) |
|||
{ |
|||
return 'SMS_003'; |
|||
} |
|||
|
|||
// 模板参数 |
|||
public function getData(GatewayInterface $gateway = null) |
|||
{ |
|||
return [ |
|||
'order_no' => $this->order->no |
|||
]; |
|||
} |
|||
} |
|||
``` |
|||
|
|||
> 更多自定义方式请参考:[`Overtrue\EasySms\Message`](Overtrue\EasySms\Message;) |
|||
|
|||
发送自定义短信: |
|||
|
|||
```php |
|||
$order = ...; |
|||
$message = new OrderPaidMessage($order); |
|||
|
|||
$easySms->send(13188888888, $message); |
|||
``` |
|||
|
|||
## 各平台配置说明 |
|||
|
|||
### [阿里云](https://www.aliyun.com/) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'aliyun' => [ |
|||
'access_key_id' => '', |
|||
'access_key_secret' => '', |
|||
'sign_name' => '', |
|||
], |
|||
``` |
|||
|
|||
### [阿里云Rest](https://www.aliyun.com/) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'aliyunrest' => [ |
|||
'app_key' => '', |
|||
'app_secret_key' => '', |
|||
'sign_name' => '', |
|||
], |
|||
``` |
|||
|
|||
### [阿里云国际](https://www.alibabacloud.com/help/zh/doc-detail/160524.html) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'aliyunintl' => [ |
|||
'access_key_id' => '', |
|||
'access_key_secret' => '', |
|||
'sign_name' => '', |
|||
], |
|||
``` |
|||
|
|||
发送示例: |
|||
|
|||
```php |
|||
use Overtrue\EasySms\PhoneNumber; |
|||
|
|||
$easySms = new EasySms($config); |
|||
$phone_number = new PhoneNumber(18888888888, 86); |
|||
|
|||
$easySms->send($phone_number, [ |
|||
'content' => '您好:先生/女士!您的验证码为${code},有效时间是5分钟,请及时验证。', |
|||
'template' => 'SMS_00000001', // 模板ID |
|||
'data' => [ |
|||
"code" => 521410, |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
### [云片](https://www.yunpian.com) |
|||
|
|||
短信内容使用 `content` |
|||
|
|||
```php |
|||
'yunpian' => [ |
|||
'api_key' => '', |
|||
'signature' => '【默认签名】', // 内容中无签名时使用 |
|||
], |
|||
``` |
|||
|
|||
### [Submail](https://www.mysubmail.com) |
|||
|
|||
短信内容使用 `data` |
|||
|
|||
```php |
|||
'submail' => [ |
|||
'app_id' => '', |
|||
'app_key' => '', |
|||
'project' => '', // 默认 project,可在发送时 data 中指定 |
|||
], |
|||
``` |
|||
|
|||
### [螺丝帽](https://luosimao.com/) |
|||
|
|||
短信内容使用 `content` |
|||
|
|||
```php |
|||
'luosimao' => [ |
|||
'api_key' => '', |
|||
], |
|||
``` |
|||
|
|||
### [容联云通讯](http://www.yuntongxun.com) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'yuntongxun' => [ |
|||
'app_id' => '', |
|||
'account_sid' => '', |
|||
'account_token' => '', |
|||
'is_sub_account' => false, |
|||
], |
|||
``` |
|||
|
|||
### [互亿无线](http://www.ihuyi.com) |
|||
|
|||
短信内容使用 `content` |
|||
|
|||
```php |
|||
'huyi' => [ |
|||
'api_id' => '', |
|||
'api_key' => '', |
|||
'signature' => '', |
|||
], |
|||
``` |
|||
|
|||
### [聚合数据](https://www.juhe.cn) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'juhe' => [ |
|||
'app_key' => '', |
|||
], |
|||
``` |
|||
|
|||
### [SendCloud](http://www.sendcloud.net/) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'sendcloud' => [ |
|||
'sms_user' => '', |
|||
'sms_key' => '', |
|||
'timestamp' => false, // 是否启用时间戳 |
|||
], |
|||
``` |
|||
### [百度云](https://cloud.baidu.com/) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'baidu' => [ |
|||
'ak' => '', |
|||
'sk' => '', |
|||
'invoke_id' => '', |
|||
'domain' => '', |
|||
], |
|||
``` |
|||
|
|||
### [华信短信平台](http://www.ipyy.com/) |
|||
|
|||
短信内容使用 `content` |
|||
|
|||
```php |
|||
'huaxin' => [ |
|||
'user_id' => '', |
|||
'password' => '', |
|||
'account' => '', |
|||
'ip' => '', |
|||
'ext_no' => '', |
|||
], |
|||
``` |
|||
|
|||
### [253云通讯(创蓝)](https://www.253.com/) |
|||
|
|||
短信内容使用 `content` |
|||
|
|||
```php |
|||
'chuanglan' => [ |
|||
'account' => '', |
|||
'password' => '', |
|||
|
|||
// 国际短信时必填 |
|||
'intel_account' => '', |
|||
'intel_password' => '', |
|||
|
|||
// \Overtrue\EasySms\Gateways\ChuanglanGateway::CHANNEL_VALIDATE_CODE => 验证码通道(默认) |
|||
// \Overtrue\EasySms\Gateways\ChuanglanGateway::CHANNEL_PROMOTION_CODE => 会员营销通道 |
|||
'channel' => \Overtrue\EasySms\Gateways\ChuanglanGateway::CHANNEL_VALIDATE_CODE, |
|||
|
|||
// 会员营销通道 特定参数。创蓝规定:api提交营销短信的时候,需要自己加短信的签名及退订信息 |
|||
'sign' => '【通讯云】', |
|||
'unsubscribe' => '回TD退订', |
|||
], |
|||
``` |
|||
|
|||
### [融云](http://www.rongcloud.cn) |
|||
|
|||
短信分为两大类,验证类和通知类短信。 发送验证类短信使用 `template` + `data` |
|||
|
|||
```php |
|||
'rongcloud' => [ |
|||
'app_key' => '', |
|||
'app_secret' => '', |
|||
] |
|||
``` |
|||
|
|||
### [天毅无线](http://www.85hu.com/) |
|||
|
|||
短信内容使用 `content` |
|||
|
|||
```php |
|||
'tianyiwuxian' => [ |
|||
'username' => '', //用户名 |
|||
'password' => '', //密码 |
|||
'gwid' => '', //网关ID |
|||
] |
|||
``` |
|||
|
|||
### [twilio](https://www.twilio.com) |
|||
|
|||
短信使用 `content` |
|||
发送对象需要 使用`+`添加区号 |
|||
|
|||
```php |
|||
'twilio' => [ |
|||
'account_sid' => '', // sid |
|||
'from' => '', // 发送的号码 可以在控制台购买 |
|||
'token' => '', // apitoken |
|||
], |
|||
``` |
|||
|
|||
### [tiniyo](https://www.tiniyo.com) |
|||
|
|||
短信使用 `content` |
|||
发送对象需要 使用`+`添加区号 |
|||
|
|||
```php |
|||
'tiniyo' => [ |
|||
'account_sid' => '', // auth_id from https://tiniyo.com |
|||
'from' => '', // 发送的号码 可以在控制台购买 |
|||
'token' => '', // auth_secret from https://tiniyo.com |
|||
], |
|||
``` |
|||
|
|||
|
|||
### [腾讯云 SMS](https://cloud.tencent.com/product/sms) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'qcloud' => [ |
|||
'sdk_app_id' => '', // 短信应用的 SDK APP ID |
|||
'secret_id' => '', // SECRET ID |
|||
'secret_key' => '', // SECRET KEY |
|||
'sign_name' => '腾讯CoDesign', // 短信签名 |
|||
], |
|||
``` |
|||
|
|||
发送示例: |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => 101234, // 模板ID |
|||
'data' => [ |
|||
"a", 'b', 'c', 'd', //按占位顺序给值 |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
### [阿凡达数据](http://www.avatardata.cn/) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'avatardata' => [ |
|||
'app_key' => '', // APP KEY |
|||
], |
|||
``` |
|||
|
|||
### [华为云 SMS](https://www.huaweicloud.com/product/msgsms.html) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'huawei' => [ |
|||
'endpoint' => '', // APP接入地址 |
|||
'app_key' => '', // APP KEY |
|||
'app_secret' => '', // APP SECRET |
|||
'from' => [ |
|||
'default' => '1069012345', // 默认使用签名通道号 |
|||
'custom' => 'csms12345', // 其他签名通道号 可以在 data 中定义 from 来指定 |
|||
'abc' => 'csms67890', // 其他签名通道号 |
|||
... |
|||
], |
|||
'callback' => '' // 短信状态回调地址 |
|||
], |
|||
``` |
|||
|
|||
使用默认签名通道 `default` |
|||
|
|||
```php |
|||
$easySms->send(13188888888, [ |
|||
'template' => 'SMS_001', |
|||
'data' => [ |
|||
6379 |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
使用指定签名通道 |
|||
|
|||
```php |
|||
$easySms->send(13188888888, [ |
|||
'template' => 'SMS_001', |
|||
'data' => [ |
|||
6379, |
|||
'from' => 'custom' // 对应 config 中的 from 数组中 custom |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
### [网易云信](https://yunxin.163.com/sms) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'yunxin' => [ |
|||
'app_key' => '', |
|||
'app_secret' => '', |
|||
'code_length' => 4, // 随机验证码长度,范围 4~10,默认为 4 |
|||
'need_up' => false, // 是否需要支持短信上行 |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => 'SMS_001', // 不填则使用默认模板 |
|||
'data' => [ |
|||
'code' => 8946, // 如果设置了该参数,则 code_length 参数无效 |
|||
'action' => 'sendCode', // 默认为 `sendCode`,校验短信验证码使用 `verifyCode` |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
### [云之讯](https://www.ucpaas.com/index.html) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'yunzhixun' => [ |
|||
'sid' => '', |
|||
'token' => '', |
|||
'app_id' => '', |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => 'SMS_001', |
|||
'data' => [ |
|||
'params' => '8946,3', // 模板参数,多个参数使用 `,` 分割,模板无参数时可为空 |
|||
'uid' => 'hexianghui', // 用户 ID,随状态报告返回,可为空 |
|||
'mobiles' => '18888888888,188888888889', // 批量发送短信,手机号使用 `,` 分割,不使用批量发送请不要设置该参数 |
|||
], |
|||
]); |
|||
``` |
|||
|
|||
### [凯信通](http://www.kingtto.cn/) |
|||
|
|||
短信内容使用 `content` |
|||
|
|||
```php |
|||
'kingtto' => [ |
|||
'userid' => '', |
|||
'account' => '', |
|||
'password' => '', |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'content' => '您的验证码为: 6379', |
|||
]); |
|||
``` |
|||
|
|||
### [七牛云](https://www.qiniu.com/) |
|||
|
|||
短信内容使用 `template` + `data` |
|||
|
|||
```php |
|||
'qiniu' => [ |
|||
'secret_key' => '', |
|||
'access_key' => '', |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => '1231234123412341234', |
|||
'data' => [ |
|||
'code' => 1234, |
|||
], |
|||
]); |
|||
``` |
|||
### [Ucloud](https://www.ucloud.cn/) |
|||
短信使用 `template` + `data` |
|||
|
|||
```php |
|||
'ucloud' => [ |
|||
'private_key' => '', //私钥 |
|||
'public_key' => '', //公钥 |
|||
'sig_content' => '', // 短信签名, |
|||
'project_id' => '', //项目ID,子账号才需要该参数 |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => 'UTAXXXXX', //短信模板 |
|||
'data' => [ |
|||
'code' => 1234, //模板参数,模板没有参数不用则填写,有多个参数请用数组,[1111,1111] |
|||
'mobiles' =>'', //同时发送多个手机短信,请用数组[xxx,xxx] |
|||
], |
|||
]); |
|||
|
|||
``` |
|||
|
|||
|
|||
### [短信宝](http://www.smsbao.com/) |
|||
短信使用 `content` |
|||
|
|||
```php |
|||
'smsbao' => [ |
|||
'user' => '', //账号 |
|||
'password' => '' //密码 |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'content' => '您的验证码为: 6379', //短信模板 |
|||
]); |
|||
|
|||
``` |
|||
|
|||
### [摩杜云](https://www.moduyun.com/) |
|||
短信使用 `template` + `data` |
|||
|
|||
```php |
|||
'moduyun' => [ |
|||
'accesskey' => '', //必填 ACCESS KEY |
|||
'secretkey' => '', //必填 SECRET KEY |
|||
'signId' => '', //选填 短信签名,如果使用默认签名,该字段可缺省 |
|||
'type' => 0, //选填 0:普通短信;1:营销短信 |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => '5a95****b953', //短信模板 |
|||
'data' => [ |
|||
1234, //模板参数,对应模板的{1} |
|||
30 //模板参数,对应模板的{2} |
|||
//... |
|||
], |
|||
]); |
|||
|
|||
``` |
|||
|
|||
### [融合云(助通)](https://www.ztinfo.cn/products/sms) |
|||
|
|||
短信使用 `template` + `data` |
|||
|
|||
```php |
|||
'rongheyun' => [ |
|||
'username' => '', //必填 用户名 |
|||
'password' => '', //必填 密码 |
|||
'signature'=> '', //必填 已报备的签名 |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => '31874', //短信模板 |
|||
'data' => [ |
|||
'valid_code' => '888888', //模板参数,对应模板的{valid_code} |
|||
//... |
|||
], |
|||
]); |
|||
|
|||
``` |
|||
|
|||
### [蜘蛛云](https://zzyun.com/) |
|||
|
|||
短信使用 `template` + `data` |
|||
|
|||
```php |
|||
'zzyun' => [ |
|||
'user_id' => '', //必填 会员ID |
|||
'secret' => '', //必填 接口密钥 |
|||
'sign_name'=> '', //必填 短信签名 |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => 'SMS_210317****', //短信模板 |
|||
'data' => [ |
|||
'code' => '888888', //模板参数,对应模板的{code} |
|||
//... |
|||
], |
|||
]); |
|||
|
|||
``` |
|||
|
|||
### [融合云信](https://maap.wo.cn/) |
|||
|
|||
短信使用 `template` + `data` |
|||
|
|||
```php |
|||
'maap' => [ |
|||
'cpcode' => '', //必填 商户编码 |
|||
'key' => '', //必填 接口密钥 |
|||
'excode'=> '', //选填 扩展名 |
|||
], |
|||
``` |
|||
|
|||
```php |
|||
$easySms->send(18888888888, [ |
|||
'template' => '356120', //短信模板 |
|||
'data' => [ |
|||
'123465' |
|||
],//模板参数 |
|||
]); |
|||
|
|||
``` |
|||
|
|||
## :heart: 支持我 |
|||
|
|||
[](https://github.com/sponsors/overtrue) |
|||
|
|||
如果你喜欢我的项目并想支持它,[点击这里 :heart:](https://github.com/sponsors/overtrue) |
|||
|
|||
## Project supported by JetBrains |
|||
|
|||
Many thanks to Jetbrains for kindly providing a license for me to work on this and other open-source projects. |
|||
|
|||
[](https://www.jetbrains.com/?from=https://github.com/overtrue) |
|||
|
|||
|
|||
## PHP 扩展包开发 |
|||
|
|||
> 想知道如何从零开始构建 PHP 扩展包? |
|||
> |
|||
> 请关注我的实战课程,我会在此课程中分享一些扩展开发经验 —— [《PHP 扩展包实战教程 - 从入门到发布》](https://learnku.com/courses/creating-package) |
|||
|
|||
## License |
|||
|
|||
MIT |
|||
@ -0,0 +1,62 @@ |
|||
{ |
|||
"name": "overtrue/easy-sms", |
|||
"description": "The easiest way to send short message.", |
|||
"type": "library", |
|||
"require": { |
|||
"guzzlehttp/guzzle": "^6.2 || ^7.0", |
|||
"php": ">=5.6", |
|||
"ext-json": "*" |
|||
}, |
|||
"require-dev": { |
|||
"phpunit/phpunit": "^5.7 || ^7.5 || ^8.5.19 || ^9.5.8", |
|||
"mockery/mockery": "~1.3.3 || ^1.4.2", |
|||
"brainmaestro/composer-git-hooks": "^2.8", |
|||
"jetbrains/phpstorm-attributes": "^1.0" |
|||
}, |
|||
"autoload": { |
|||
"psr-4": { |
|||
"Overtrue\\EasySms\\": "src" |
|||
} |
|||
}, |
|||
"autoload-dev": { |
|||
"psr-4": { |
|||
"Overtrue\\EasySms\\Tests\\": "tests" |
|||
} |
|||
}, |
|||
"license": "MIT", |
|||
"authors": [{ |
|||
"name": "overtrue", |
|||
"email": "i@overtrue.me" |
|||
}], |
|||
"extra": { |
|||
"hooks": { |
|||
"pre-commit": [ |
|||
"composer check-style", |
|||
"composer psalm", |
|||
"composer test" |
|||
], |
|||
"pre-push": [ |
|||
"composer check-style" |
|||
] |
|||
} |
|||
}, |
|||
"scripts": { |
|||
"post-update-cmd": [ |
|||
"cghooks remove", |
|||
"cghooks add --ignore-lock", |
|||
"cghooks update" |
|||
], |
|||
"post-merge": "composer install", |
|||
"post-install-cmd": [ |
|||
"cghooks remove", |
|||
"cghooks add --ignore-lock", |
|||
"cghooks update" |
|||
], |
|||
"phpstan": "phpstan analyse", |
|||
"check-style": "php-cs-fixer fix --using-cache=no --diff --config=.php-cs-fixer.dist.php --dry-run --allow-risky=yes --ansi", |
|||
"fix-style": "php-cs-fixer fix --using-cache=no --config=.php-cs-fixer.dist.php --allow-risky=yes --ansi", |
|||
"test": "phpunit --colors", |
|||
"psalm": "psalm --show-info=true --no-cache", |
|||
"psalm-fix": "psalm --no-cache --alter --issues=MissingReturnType,MissingParamType" |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
<?xml version="1.0"?> |
|||
<psalm |
|||
errorLevel="6" |
|||
resolveFromConfigFile="true" |
|||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
|||
xmlns="https://getpsalm.org/schema/config" |
|||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" |
|||
> |
|||
<projectFiles> |
|||
<directory name="src" /> |
|||
<ignoreFiles> |
|||
<directory name="vendor" /> |
|||
</ignoreFiles> |
|||
</projectFiles> |
|||
</psalm> |
|||
@ -0,0 +1,38 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Contracts; |
|||
|
|||
use Overtrue\EasySms\Support\Config; |
|||
|
|||
/** |
|||
* Class GatewayInterface. |
|||
*/ |
|||
interface GatewayInterface |
|||
{ |
|||
/** |
|||
* Get gateway name. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getName(); |
|||
|
|||
/** |
|||
* Send a short message. |
|||
* |
|||
* @param \Overtrue\EasySms\Contracts\PhoneNumberInterface $to |
|||
* @param \Overtrue\EasySms\Contracts\MessageInterface $message |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config); |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Contracts; |
|||
|
|||
/** |
|||
* Interface MessageInterface. |
|||
*/ |
|||
interface MessageInterface |
|||
{ |
|||
const TEXT_MESSAGE = 'text'; |
|||
|
|||
const VOICE_MESSAGE = 'voice'; |
|||
|
|||
/** |
|||
* Return the message type. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getMessageType(); |
|||
|
|||
/** |
|||
* Return message content. |
|||
* |
|||
* @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getContent(GatewayInterface $gateway = null); |
|||
|
|||
/** |
|||
* Return the template id of message. |
|||
* |
|||
* @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getTemplate(GatewayInterface $gateway = null); |
|||
|
|||
/** |
|||
* Return the template data of message. |
|||
* |
|||
* @param \Overtrue\EasySms\Contracts\GatewayInterface|null $gateway |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getData(GatewayInterface $gateway = null); |
|||
|
|||
/** |
|||
* Return message supported gateways. |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function getGateways(); |
|||
} |
|||
@ -0,0 +1,53 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Contracts; |
|||
|
|||
/** |
|||
* Interface PhoneNumberInterface. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
interface PhoneNumberInterface extends \JsonSerializable |
|||
{ |
|||
/** |
|||
* 86. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getIDDCode(); |
|||
|
|||
/** |
|||
* 18888888888. |
|||
* |
|||
* @return int |
|||
*/ |
|||
public function getNumber(); |
|||
|
|||
/** |
|||
* +8618888888888. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getUniversalNumber(); |
|||
|
|||
/** |
|||
* 008618888888888. |
|||
* |
|||
* @return string |
|||
*/ |
|||
public function getZeroPrefixedNumber(); |
|||
|
|||
/** |
|||
* @return string |
|||
*/ |
|||
public function __toString(); |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Contracts; |
|||
|
|||
/** |
|||
* Interface StrategyInterface. |
|||
*/ |
|||
interface StrategyInterface |
|||
{ |
|||
/** |
|||
* Apply the strategy and return result. |
|||
* |
|||
* @param array $gateways |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function apply(array $gateways); |
|||
} |
|||
@ -0,0 +1,326 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms; |
|||
|
|||
use Closure; |
|||
use Overtrue\EasySms\Contracts\GatewayInterface; |
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Contracts\StrategyInterface; |
|||
use Overtrue\EasySms\Exceptions\InvalidArgumentException; |
|||
use Overtrue\EasySms\Gateways\Gateway; |
|||
use Overtrue\EasySms\Strategies\OrderStrategy; |
|||
use Overtrue\EasySms\Support\Config; |
|||
|
|||
/** |
|||
* Class EasySms. |
|||
*/ |
|||
class EasySms |
|||
{ |
|||
/** |
|||
* @var \Overtrue\EasySms\Support\Config |
|||
*/ |
|||
protected $config; |
|||
|
|||
/** |
|||
* @var string |
|||
*/ |
|||
protected $defaultGateway; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $customCreators = []; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $gateways = []; |
|||
|
|||
/** |
|||
* @var \Overtrue\EasySms\Messenger |
|||
*/ |
|||
protected $messenger; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $strategies = []; |
|||
|
|||
/** |
|||
* Constructor. |
|||
* |
|||
* @param array $config |
|||
*/ |
|||
public function __construct(array $config) |
|||
{ |
|||
$this->config = new Config($config); |
|||
} |
|||
|
|||
/** |
|||
* Send a message. |
|||
* |
|||
* @param string|array $to |
|||
* @param \Overtrue\EasySms\Contracts\MessageInterface|array $message |
|||
* @param array $gateways |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException |
|||
* @throws \Overtrue\EasySms\Exceptions\NoGatewayAvailableException |
|||
*/ |
|||
public function send($to, $message, array $gateways = []) |
|||
{ |
|||
$to = $this->formatPhoneNumber($to); |
|||
$message = $this->formatMessage($message); |
|||
$gateways = empty($gateways) ? $message->getGateways() : $gateways; |
|||
|
|||
if (empty($gateways)) { |
|||
$gateways = $this->config->get('default.gateways', []); |
|||
} |
|||
|
|||
return $this->getMessenger()->send($to, $message, $this->formatGateways($gateways)); |
|||
} |
|||
|
|||
/** |
|||
* Create a gateway. |
|||
* |
|||
* @param string|null $name |
|||
* |
|||
* @return \Overtrue\EasySms\Contracts\GatewayInterface |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException |
|||
*/ |
|||
public function gateway($name) |
|||
{ |
|||
if (!isset($this->gateways[$name])) { |
|||
$this->gateways[$name] = $this->createGateway($name); |
|||
} |
|||
|
|||
return $this->gateways[$name]; |
|||
} |
|||
|
|||
/** |
|||
* Get a strategy instance. |
|||
* |
|||
* @param string|null $strategy |
|||
* |
|||
* @return \Overtrue\EasySms\Contracts\StrategyInterface |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException |
|||
*/ |
|||
public function strategy($strategy = null) |
|||
{ |
|||
if (\is_null($strategy)) { |
|||
$strategy = $this->config->get('default.strategy', OrderStrategy::class); |
|||
} |
|||
|
|||
if (!\class_exists($strategy)) { |
|||
$strategy = __NAMESPACE__.'\Strategies\\'.\ucfirst($strategy); |
|||
} |
|||
|
|||
if (!\class_exists($strategy)) { |
|||
throw new InvalidArgumentException("Unsupported strategy \"{$strategy}\""); |
|||
} |
|||
|
|||
if (empty($this->strategies[$strategy]) || !($this->strategies[$strategy] instanceof StrategyInterface)) { |
|||
$this->strategies[$strategy] = new $strategy($this); |
|||
} |
|||
|
|||
return $this->strategies[$strategy]; |
|||
} |
|||
|
|||
/** |
|||
* Register a custom driver creator Closure. |
|||
* |
|||
* @param string $name |
|||
* @param \Closure $callback |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function extend($name, Closure $callback) |
|||
{ |
|||
$this->customCreators[$name] = $callback; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return \Overtrue\EasySms\Support\Config |
|||
*/ |
|||
public function getConfig() |
|||
{ |
|||
return $this->config; |
|||
} |
|||
|
|||
/** |
|||
* @return \Overtrue\EasySms\Messenger |
|||
*/ |
|||
public function getMessenger() |
|||
{ |
|||
return $this->messenger ?: $this->messenger = new Messenger($this); |
|||
} |
|||
|
|||
/** |
|||
* Create a new driver instance. |
|||
* |
|||
* @param string $name |
|||
* |
|||
* @throws \InvalidArgumentException |
|||
* |
|||
* @return GatewayInterface |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException |
|||
*/ |
|||
protected function createGateway($name) |
|||
{ |
|||
$config = $this->config->get("gateways.{$name}", []); |
|||
|
|||
if (!isset($config['timeout'])) { |
|||
$config['timeout'] = $this->config->get('timeout', Gateway::DEFAULT_TIMEOUT); |
|||
} |
|||
|
|||
$config['options'] = $this->config->get('options', []); |
|||
|
|||
if (isset($this->customCreators[$name])) { |
|||
$gateway = $this->callCustomCreator($name, $config); |
|||
} else { |
|||
$className = $this->formatGatewayClassName($name); |
|||
$gateway = $this->makeGateway($className, $config); |
|||
} |
|||
|
|||
if (!($gateway instanceof GatewayInterface)) { |
|||
throw new InvalidArgumentException(\sprintf('Gateway "%s" must implement interface %s.', $name, GatewayInterface::class)); |
|||
} |
|||
|
|||
return $gateway; |
|||
} |
|||
|
|||
/** |
|||
* Make gateway instance. |
|||
* |
|||
* @param string $gateway |
|||
* @param array $config |
|||
* |
|||
* @return \Overtrue\EasySms\Contracts\GatewayInterface |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException |
|||
*/ |
|||
protected function makeGateway($gateway, $config) |
|||
{ |
|||
if (!\class_exists($gateway) || !\in_array(GatewayInterface::class, \class_implements($gateway))) { |
|||
throw new InvalidArgumentException(\sprintf('Class "%s" is a invalid easy-sms gateway.', $gateway)); |
|||
} |
|||
|
|||
return new $gateway($config); |
|||
} |
|||
|
|||
/** |
|||
* Format gateway name. |
|||
* |
|||
* @param string $name |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function formatGatewayClassName($name) |
|||
{ |
|||
if (\class_exists($name) && \in_array(GatewayInterface::class, \class_implements($name))) { |
|||
return $name; |
|||
} |
|||
|
|||
$name = \ucfirst(\str_replace(['-', '_', ''], '', $name)); |
|||
|
|||
return __NAMESPACE__."\\Gateways\\{$name}Gateway"; |
|||
} |
|||
|
|||
/** |
|||
* Call a custom gateway creator. |
|||
* |
|||
* @param string $gateway |
|||
* @param array $config |
|||
* |
|||
* @return mixed |
|||
*/ |
|||
protected function callCustomCreator($gateway, $config) |
|||
{ |
|||
return \call_user_func($this->customCreators[$gateway], $config); |
|||
} |
|||
|
|||
/** |
|||
* @param string|\Overtrue\EasySms\Contracts\PhoneNumberInterface $number |
|||
* |
|||
* @return \Overtrue\EasySms\Contracts\PhoneNumberInterface|string |
|||
*/ |
|||
protected function formatPhoneNumber($number) |
|||
{ |
|||
if ($number instanceof PhoneNumberInterface) { |
|||
return $number; |
|||
} |
|||
|
|||
return new PhoneNumber(\trim($number)); |
|||
} |
|||
|
|||
/** |
|||
* @param array|string|\Overtrue\EasySms\Contracts\MessageInterface $message |
|||
* |
|||
* @return \Overtrue\EasySms\Contracts\MessageInterface |
|||
*/ |
|||
protected function formatMessage($message) |
|||
{ |
|||
if (!($message instanceof MessageInterface)) { |
|||
if (!\is_array($message)) { |
|||
$message = [ |
|||
'content' => $message, |
|||
'template' => $message, |
|||
]; |
|||
} |
|||
|
|||
$message = new Message($message); |
|||
} |
|||
|
|||
return $message; |
|||
} |
|||
|
|||
/** |
|||
* @param array $gateways |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\InvalidArgumentException |
|||
*/ |
|||
protected function formatGateways(array $gateways) |
|||
{ |
|||
$formatted = []; |
|||
|
|||
foreach ($gateways as $gateway => $setting) { |
|||
if (\is_int($gateway) && \is_string($setting)) { |
|||
$gateway = $setting; |
|||
$setting = []; |
|||
} |
|||
|
|||
$formatted[$gateway] = $setting; |
|||
$globalSettings = $this->config->get("gateways.{$gateway}", []); |
|||
|
|||
if (\is_string($gateway) && !empty($globalSettings) && \is_array($setting)) { |
|||
$formatted[$gateway] = new Config(\array_merge($globalSettings, $setting)); |
|||
} |
|||
} |
|||
|
|||
$result = []; |
|||
|
|||
foreach ($this->strategy()->apply($formatted) as $name) { |
|||
$result[$name] = $formatted[$name]; |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Exceptions; |
|||
|
|||
/** |
|||
* Class Exception. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class Exception extends \Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,37 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Exceptions; |
|||
|
|||
/** |
|||
* Class GatewayErrorException. |
|||
*/ |
|||
class GatewayErrorException extends Exception |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
public $raw = []; |
|||
|
|||
/** |
|||
* GatewayErrorException constructor. |
|||
* |
|||
* @param string $message |
|||
* @param int $code |
|||
* @param array $raw |
|||
*/ |
|||
public function __construct($message, $code, array $raw = []) |
|||
{ |
|||
parent::__construct($message, intval($code)); |
|||
|
|||
$this->raw = $raw; |
|||
} |
|||
} |
|||
@ -0,0 +1,19 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Exceptions; |
|||
|
|||
/** |
|||
* Class InvalidArgumentException. |
|||
*/ |
|||
class InvalidArgumentException extends Exception |
|||
{ |
|||
} |
|||
@ -0,0 +1,81 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Exceptions; |
|||
|
|||
use Throwable; |
|||
|
|||
/** |
|||
* Class NoGatewayAvailableException. |
|||
* |
|||
* @author overtrue <i@overtrue.me> |
|||
*/ |
|||
class NoGatewayAvailableException extends Exception |
|||
{ |
|||
/** |
|||
* @var array |
|||
*/ |
|||
public $results = []; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
public $exceptions = []; |
|||
|
|||
/** |
|||
* NoGatewayAvailableException constructor. |
|||
* |
|||
* @param array $results |
|||
* @param int $code |
|||
* @param \Throwable|null $previous |
|||
*/ |
|||
public function __construct(array $results = [], $code = 0, Throwable $previous = null) |
|||
{ |
|||
$this->results = $results; |
|||
$this->exceptions = \array_column($results, 'exception', 'gateway'); |
|||
|
|||
parent::__construct('All the gateways have failed. You can get error details by `$exception->getExceptions()`', $code, $previous); |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function getResults() |
|||
{ |
|||
return $this->results; |
|||
} |
|||
|
|||
/** |
|||
* @param string $gateway |
|||
* |
|||
* @return mixed|null |
|||
*/ |
|||
public function getException($gateway) |
|||
{ |
|||
return isset($this->exceptions[$gateway]) ? $this->exceptions[$gateway] : null; |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function getExceptions() |
|||
{ |
|||
return $this->exceptions; |
|||
} |
|||
|
|||
/** |
|||
* @return mixed |
|||
*/ |
|||
public function getLastException() |
|||
{ |
|||
return end($this->exceptions); |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
/** |
|||
* Class AliyunGateway. |
|||
* |
|||
* @author carson <docxcn@gmail.com> |
|||
* |
|||
* @see https://help.aliyun.com/document_detail/55451.html |
|||
*/ |
|||
class AliyunGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
const ENDPOINT_URL = 'http://dysmsapi.aliyuncs.com'; |
|||
|
|||
const ENDPOINT_METHOD = 'SendSms'; |
|||
|
|||
const ENDPOINT_VERSION = '2017-05-25'; |
|||
|
|||
const ENDPOINT_FORMAT = 'JSON'; |
|||
|
|||
const ENDPOINT_REGION_ID = 'cn-hangzhou'; |
|||
|
|||
const ENDPOINT_SIGNATURE_METHOD = 'HMAC-SHA1'; |
|||
|
|||
const ENDPOINT_SIGNATURE_VERSION = '1.0'; |
|||
|
|||
/** |
|||
* @param \Overtrue\EasySms\Contracts\PhoneNumberInterface $to |
|||
* @param \Overtrue\EasySms\Contracts\MessageInterface $message |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\GatewayErrorException ; |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$data = $message->getData($this); |
|||
|
|||
$signName = !empty($data['sign_name']) ? $data['sign_name'] : $config->get('sign_name'); |
|||
|
|||
unset($data['sign_name']); |
|||
|
|||
$params = [ |
|||
'RegionId' => self::ENDPOINT_REGION_ID, |
|||
'AccessKeyId' => $config->get('access_key_id'), |
|||
'Format' => self::ENDPOINT_FORMAT, |
|||
'SignatureMethod' => self::ENDPOINT_SIGNATURE_METHOD, |
|||
'SignatureVersion' => self::ENDPOINT_SIGNATURE_VERSION, |
|||
'SignatureNonce' => uniqid(), |
|||
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), |
|||
'Action' => self::ENDPOINT_METHOD, |
|||
'Version' => self::ENDPOINT_VERSION, |
|||
'PhoneNumbers' => !\is_null($to->getIDDCode()) ? strval($to->getZeroPrefixedNumber()) : $to->getNumber(), |
|||
'SignName' => $signName, |
|||
'TemplateCode' => $message->getTemplate($this), |
|||
'TemplateParam' => json_encode($data, JSON_FORCE_OBJECT), |
|||
]; |
|||
|
|||
$params['Signature'] = $this->generateSign($params); |
|||
|
|||
$result = $this->get(self::ENDPOINT_URL, $params); |
|||
|
|||
if ('OK' != $result['Code']) { |
|||
throw new GatewayErrorException($result['Message'], $result['Code'], $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Generate Sign. |
|||
* |
|||
* @param array $params |
|||
* |
|||
* @return string |
|||
* |
|||
* @see https://help.aliyun.com/document_detail/101343.html |
|||
*/ |
|||
protected function generateSign($params) |
|||
{ |
|||
ksort($params); |
|||
$accessKeySecret = $this->config->get('access_key_secret'); |
|||
$stringToSign = 'GET&%2F&'.urlencode(http_build_query($params, '', '&', PHP_QUERY_RFC3986)); |
|||
$stringToSign = str_replace('%7E', '~', $stringToSign); |
|||
|
|||
return base64_encode(hash_hmac('sha1', $stringToSign, $accessKeySecret.'&', true)); |
|||
} |
|||
} |
|||
@ -0,0 +1,97 @@ |
|||
<?php |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
/** |
|||
* Class AliyunIntlGateway |
|||
* |
|||
* @package \Overtrue\EasySms\Gateways |
|||
* |
|||
* @see https://www.alibabacloud.com/help/zh/doc-detail/162279.htm |
|||
*/ |
|||
class AliyunIntlGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
const ENDPOINT_URL = 'https://dysmsapi.ap-southeast-1.aliyuncs.com'; |
|||
|
|||
const ENDPOINT_ACTION = 'SendMessageWithTemplate'; |
|||
|
|||
const ENDPOINT_VERSION = '2018-05-01'; |
|||
|
|||
const ENDPOINT_FORMAT = 'JSON'; |
|||
|
|||
const ENDPOINT_REGION_ID = 'ap-southeast-1'; |
|||
|
|||
const ENDPOINT_SIGNATURE_METHOD = 'HMAC-SHA1'; |
|||
|
|||
const ENDPOINT_SIGNATURE_VERSION = '1.0'; |
|||
|
|||
|
|||
/** |
|||
* @param \Overtrue\EasySms\Contracts\PhoneNumberInterface $to |
|||
* @param \Overtrue\EasySms\Contracts\MessageInterface $message |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\GatewayErrorException |
|||
* |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$data = $message->getData($this); |
|||
|
|||
$signName = !empty($data['sign_name']) ? $data['sign_name'] : $config->get('sign_name'); |
|||
|
|||
unset($data['sign_name']); |
|||
|
|||
$params = [ |
|||
'RegionId' => self::ENDPOINT_REGION_ID, |
|||
'AccessKeyId' => $config->get('access_key_id'), |
|||
'Format' => self::ENDPOINT_FORMAT, |
|||
'SignatureMethod' => self::ENDPOINT_SIGNATURE_METHOD, |
|||
'SignatureVersion' => self::ENDPOINT_SIGNATURE_VERSION, |
|||
'SignatureNonce' => uniqid('', true), |
|||
'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), |
|||
'Version' => self::ENDPOINT_VERSION, |
|||
'To' => !\is_null($to->getIDDCode()) ? (int) $to->getZeroPrefixedNumber() : $to->getNumber(), |
|||
'Action' => self::ENDPOINT_ACTION, |
|||
'From' => $signName, |
|||
'TemplateCode' => $message->getTemplate($this), |
|||
'TemplateParam' => json_encode($data, JSON_FORCE_OBJECT), |
|||
]; |
|||
|
|||
$params['Signature'] = $this->generateSign($params); |
|||
|
|||
$result = $this->get(self::ENDPOINT_URL, $params); |
|||
|
|||
if ('OK' !== $result['ResponseCode']) { |
|||
throw new GatewayErrorException($result['ResponseDescription'], $result['ResponseCode'], $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Generate sign |
|||
* |
|||
* @param array $params |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function generateSign(array $params) |
|||
{ |
|||
ksort($params); |
|||
$accessKeySecret = $this->config->get('access_key_secret'); |
|||
$stringToSign = 'GET&%2F&'.urlencode(http_build_query($params, '', '&', PHP_QUERY_RFC3986)); |
|||
|
|||
return base64_encode(hash_hmac('sha1', $stringToSign, $accessKeySecret.'&', true)); |
|||
} |
|||
} |
|||
@ -0,0 +1,107 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
/** |
|||
* Class AliyunrestGateway. |
|||
*/ |
|||
class AliyunrestGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
const ENDPOINT_URL = 'http://gw.api.taobao.com/router/rest'; |
|||
|
|||
const ENDPOINT_VERSION = '2.0'; |
|||
|
|||
const ENDPOINT_FORMAT = 'json'; |
|||
|
|||
const ENDPOINT_METHOD = 'alibaba.aliqin.fc.sms.num.send'; |
|||
|
|||
const ENDPOINT_SIGNATURE_METHOD = 'md5'; |
|||
|
|||
const ENDPOINT_PARTNER_ID = 'EasySms'; |
|||
|
|||
/** |
|||
* @param PhoneNumberInterface $to |
|||
* @param MessageInterface $message |
|||
* @param Config $config |
|||
* |
|||
* @return array|void |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$urlParams = [ |
|||
'app_key' => $config->get('app_key'), |
|||
'v' => self::ENDPOINT_VERSION, |
|||
'format' => self::ENDPOINT_FORMAT, |
|||
'sign_method' => self::ENDPOINT_SIGNATURE_METHOD, |
|||
'method' => self::ENDPOINT_METHOD, |
|||
'timestamp' => date('Y-m-d H:i:s'), |
|||
'partner_id' => self::ENDPOINT_PARTNER_ID, |
|||
]; |
|||
|
|||
$params = [ |
|||
'extend' => '', |
|||
'sms_type' => 'normal', |
|||
'sms_free_sign_name' => $config->get('sign_name'), |
|||
'sms_param' => json_encode($message->getData($this)), |
|||
'rec_num' => !\is_null($to->getIDDCode()) ? strval($to->getZeroPrefixedNumber()) : $to->getNumber(), |
|||
'sms_template_code' => $message->getTemplate($this), |
|||
]; |
|||
$urlParams['sign'] = $this->generateSign(array_merge($params, $urlParams)); |
|||
|
|||
$result = $this->post($this->getEndpointUrl($urlParams), $params); |
|||
|
|||
if (isset($result['error_response']) && 0 != $result['error_response']['code']) { |
|||
throw new GatewayErrorException($result['error_response']['msg'], $result['error_response']['code'], $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* @param array $params |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getEndpointUrl($params) |
|||
{ |
|||
return self::ENDPOINT_URL.'?'.http_build_query($params); |
|||
} |
|||
|
|||
/** |
|||
* @param array $params |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function generateSign($params) |
|||
{ |
|||
ksort($params); |
|||
|
|||
$stringToBeSigned = $this->config->get('app_secret_key'); |
|||
foreach ($params as $k => $v) { |
|||
if (!is_array($v) && '@' != substr($v, 0, 1)) { |
|||
$stringToBeSigned .= "$k$v"; |
|||
} |
|||
} |
|||
unset($k, $v); |
|||
$stringToBeSigned .= $this->config->get('app_secret_key'); |
|||
|
|||
return strtoupper(md5($stringToBeSigned)); |
|||
} |
|||
} |
|||
@ -0,0 +1,60 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
/** |
|||
* Class AvatardataGateway. |
|||
* |
|||
* @see http://www.avatardata.cn/Docs/Api/fd475e40-7809-4be7-936c-5926dd41b0fe |
|||
*/ |
|||
class AvatardataGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
const ENDPOINT_URL = 'http://v1.avatardata.cn/Sms/Send'; |
|||
|
|||
const ENDPOINT_FORMAT = 'json'; |
|||
|
|||
/** |
|||
* @param PhoneNumberInterface $to |
|||
* @param MessageInterface $message |
|||
* @param Config $config |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws GatewayErrorException; |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$params = [ |
|||
'mobile' => $to->getNumber(), |
|||
'templateId' => $message->getTemplate($this), |
|||
'param' => implode(',', $message->getData($this)), |
|||
'dtype' => self::ENDPOINT_FORMAT, |
|||
'key' => $config->get('app_key'), |
|||
]; |
|||
|
|||
$result = $this->get(self::ENDPOINT_URL, $params); |
|||
|
|||
if ($result['error_code']) { |
|||
throw new GatewayErrorException($result['reason'], $result['error_code'], $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
} |
|||
@ -0,0 +1,174 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
/** |
|||
* Class BaiduGateway. |
|||
* |
|||
* @see https://cloud.baidu.com/doc/SMS/index.html |
|||
*/ |
|||
class BaiduGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
const ENDPOINT_HOST = 'smsv3.bj.baidubce.com'; |
|||
|
|||
const ENDPOINT_URI = '/api/v3/sendSms'; |
|||
|
|||
const BCE_AUTH_VERSION = 'bce-auth-v1'; |
|||
|
|||
const DEFAULT_EXPIRATION_IN_SECONDS = 1800; //签名有效期默认1800秒 |
|||
|
|||
const SUCCESS_CODE = 1000; |
|||
|
|||
/** |
|||
* Send message. |
|||
* |
|||
* @param \Overtrue\EasySms\Contracts\PhoneNumberInterface $to |
|||
* @param \Overtrue\EasySms\Contracts\MessageInterface $message |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\GatewayErrorException ; |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$params = [ |
|||
'signatureId' => $config->get('invoke_id'), |
|||
'mobile' => $to->getNumber(), |
|||
'template' => $message->getTemplate($this), |
|||
'contentVar' => $message->getData($this), |
|||
]; |
|||
if (!empty($params['contentVar']['custom'])) { |
|||
//用户自定义参数,格式为字符串,状态回调时会回传该值 |
|||
$params['custom'] = $params['contentVar']['custom']; |
|||
unset($params['contentVar']['custom']); |
|||
} |
|||
if (!empty($params['contentVar']['userExtId'])) { |
|||
//通道自定义扩展码,上行回调时会回传该值,其格式为纯数字串。默认为不开通,请求时无需设置该参数。如需开通请联系客服申请 |
|||
$params['userExtId'] = $params['contentVar']['userExtId']; |
|||
unset($params['contentVar']['userExtId']); |
|||
} |
|||
|
|||
$datetime = gmdate('Y-m-d\TH:i:s\Z'); |
|||
|
|||
$headers = [ |
|||
'host' => self::ENDPOINT_HOST, |
|||
'content-type' => 'application/json', |
|||
'x-bce-date' => $datetime, |
|||
]; |
|||
//获得需要签名的数据 |
|||
$signHeaders = $this->getHeadersToSign($headers, ['host', 'x-bce-date']); |
|||
|
|||
$headers['Authorization'] = $this->generateSign($signHeaders, $datetime, $config); |
|||
|
|||
$result = $this->request('post', self::buildEndpoint($config), ['headers' => $headers, 'json' => $params]); |
|||
|
|||
if (self::SUCCESS_CODE != $result['code']) { |
|||
throw new GatewayErrorException($result['message'], $result['code'], $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Build endpoint url. |
|||
* |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function buildEndpoint(Config $config) |
|||
{ |
|||
return 'http://'.$config->get('domain', self::ENDPOINT_HOST).self::ENDPOINT_URI; |
|||
} |
|||
|
|||
/** |
|||
* Generate Authorization header. |
|||
* |
|||
* @param array $signHeaders |
|||
* @param int $datetime |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function generateSign(array $signHeaders, $datetime, Config $config) |
|||
{ |
|||
// 生成 authString |
|||
$authString = self::BCE_AUTH_VERSION.'/'.$config->get('ak').'/' |
|||
.$datetime.'/'.self::DEFAULT_EXPIRATION_IN_SECONDS; |
|||
|
|||
// 使用 sk 和 authString 生成 signKey |
|||
$signingKey = hash_hmac('sha256', $authString, $config->get('sk')); |
|||
// 生成标准化 URI |
|||
// 根据 RFC 3986,除了:1.大小写英文字符 2.阿拉伯数字 3.点'.'、波浪线'~'、减号'-'以及下划线'_' 以外都要编码 |
|||
$canonicalURI = str_replace('%2F', '/', rawurlencode(self::ENDPOINT_URI)); |
|||
|
|||
// 生成标准化 QueryString |
|||
$canonicalQueryString = ''; // 此 api 不需要此项。返回空字符串 |
|||
|
|||
// 整理 headersToSign,以 ';' 号连接 |
|||
$signedHeaders = empty($signHeaders) ? '' : strtolower(trim(implode(';', array_keys($signHeaders)))); |
|||
|
|||
// 生成标准化 header |
|||
$canonicalHeader = $this->getCanonicalHeaders($signHeaders); |
|||
|
|||
// 组成标准请求串 |
|||
$canonicalRequest = "POST\n{$canonicalURI}\n{$canonicalQueryString}\n{$canonicalHeader}"; |
|||
|
|||
// 使用 signKey 和标准请求串完成签名 |
|||
$signature = hash_hmac('sha256', $canonicalRequest, $signingKey); |
|||
|
|||
// 组成最终签名串 |
|||
return "{$authString}/{$signedHeaders}/{$signature}"; |
|||
} |
|||
|
|||
/** |
|||
* 生成标准化 http 请求头串. |
|||
* |
|||
* @param array $headers |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getCanonicalHeaders(array $headers) |
|||
{ |
|||
$headerStrings = []; |
|||
foreach ($headers as $name => $value) { |
|||
//trim后再encode,之后使用':'号连接起来 |
|||
$headerStrings[] = rawurlencode(strtolower(trim($name))).':'.rawurlencode(trim($value)); |
|||
} |
|||
|
|||
sort($headerStrings); |
|||
|
|||
return implode("\n", $headerStrings); |
|||
} |
|||
|
|||
/** |
|||
* 根据 指定的 keys 过滤应该参与签名的 header. |
|||
* |
|||
* @param array $headers |
|||
* @param array $keys |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getHeadersToSign(array $headers, array $keys) |
|||
{ |
|||
return array_intersect_key($headers, array_flip($keys)); |
|||
} |
|||
} |
|||
@ -0,0 +1,156 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Exceptions\InvalidArgumentException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
/** |
|||
* Class ChuanglanGateway. |
|||
* |
|||
* @see https://zz.253.com/v5.html#/api_doc |
|||
*/ |
|||
class ChuanglanGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
/** |
|||
* URL模板 |
|||
*/ |
|||
const ENDPOINT_URL_TEMPLATE = 'https://%s.253.com/msg/send/json'; |
|||
|
|||
/** |
|||
* 国际短信 |
|||
*/ |
|||
const INT_URL = 'http://intapi.253.com/send/json'; |
|||
|
|||
/** |
|||
* 验证码渠道code. |
|||
*/ |
|||
const CHANNEL_VALIDATE_CODE = 'smsbj1'; |
|||
|
|||
/** |
|||
* 会员营销渠道code. |
|||
*/ |
|||
const CHANNEL_PROMOTION_CODE = 'smssh1'; |
|||
|
|||
/** |
|||
* @param PhoneNumberInterface $to |
|||
* @param MessageInterface $message |
|||
* @param Config $config |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws GatewayErrorException |
|||
* @throws InvalidArgumentException |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$IDDCode = !empty($to->getIDDCode()) ? $to->getIDDCode() : 86; |
|||
|
|||
$params = [ |
|||
'account' => $config->get('account'), |
|||
'password' => $config->get('password'), |
|||
'phone' => $to->getNumber(), |
|||
'msg' => $this->wrapChannelContent($message->getContent($this), $config, $IDDCode), |
|||
]; |
|||
|
|||
if (86 != $IDDCode) { |
|||
$params['mobile'] = $to->getIDDCode().$to->getNumber(); |
|||
$params['account'] = $config->get('intel_account') ?: $config->get('account'); |
|||
$params['password'] = $config->get('intel_password') ?: $config->get('password'); |
|||
} |
|||
|
|||
$result = $this->postJson($this->buildEndpoint($config, $IDDCode), $params); |
|||
|
|||
if (!isset($result['code']) || '0' != $result['code']) { |
|||
throw new GatewayErrorException(json_encode($result, JSON_UNESCAPED_UNICODE), isset($result['code']) ? $result['code'] : 0, $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* @param Config $config |
|||
* @param int $IDDCode |
|||
* |
|||
* @return string |
|||
* |
|||
* @throws InvalidArgumentException |
|||
*/ |
|||
protected function buildEndpoint(Config $config, $IDDCode = 86) |
|||
{ |
|||
$channel = $this->getChannel($config, $IDDCode); |
|||
|
|||
if (self::INT_URL === $channel) { |
|||
return $channel; |
|||
} |
|||
|
|||
return sprintf(self::ENDPOINT_URL_TEMPLATE, $channel); |
|||
} |
|||
|
|||
/** |
|||
* @param Config $config |
|||
* @param int $IDDCode |
|||
* |
|||
* @return mixed |
|||
* |
|||
* @throws InvalidArgumentException |
|||
*/ |
|||
protected function getChannel(Config $config, $IDDCode) |
|||
{ |
|||
if (86 != $IDDCode) { |
|||
return self::INT_URL; |
|||
} |
|||
$channel = $config->get('channel', self::CHANNEL_VALIDATE_CODE); |
|||
|
|||
if (!in_array($channel, [self::CHANNEL_VALIDATE_CODE, self::CHANNEL_PROMOTION_CODE])) { |
|||
throw new InvalidArgumentException('Invalid channel for ChuanglanGateway.'); |
|||
} |
|||
|
|||
return $channel; |
|||
} |
|||
|
|||
/** |
|||
* @param string $content |
|||
* @param Config $config |
|||
* @param int $IDDCode |
|||
* |
|||
* @return string|string |
|||
* |
|||
* @throws InvalidArgumentException |
|||
*/ |
|||
protected function wrapChannelContent($content, Config $config, $IDDCode) |
|||
{ |
|||
$channel = $this->getChannel($config, $IDDCode); |
|||
|
|||
if (self::CHANNEL_PROMOTION_CODE == $channel) { |
|||
$sign = (string) $config->get('sign', ''); |
|||
if (empty($sign)) { |
|||
throw new InvalidArgumentException('Invalid sign for ChuanglanGateway when using promotion channel'); |
|||
} |
|||
|
|||
$unsubscribe = (string) $config->get('unsubscribe', ''); |
|||
if (empty($unsubscribe)) { |
|||
throw new InvalidArgumentException('Invalid unsubscribe for ChuanglanGateway when using promotion channel'); |
|||
} |
|||
|
|||
$content = $sign.$content.$unsubscribe; |
|||
} |
|||
|
|||
return $content; |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Support\Config; |
|||
|
|||
/** |
|||
* Class ErrorlogGateway. |
|||
*/ |
|||
class ErrorlogGateway extends Gateway |
|||
{ |
|||
/** |
|||
* @param \Overtrue\EasySms\Contracts\PhoneNumberInterface $to |
|||
* @param \Overtrue\EasySms\Contracts\MessageInterface $message |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return array |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
if (is_array($to)) { |
|||
$to = implode(',', $to); |
|||
} |
|||
|
|||
$message = sprintf( |
|||
"[%s] to: %s | message: \"%s\" | template: \"%s\" | data: %s\n", |
|||
date('Y-m-d H:i:s'), |
|||
$to, |
|||
$message->getContent($this), |
|||
$message->getTemplate($this), |
|||
json_encode($message->getData($this)) |
|||
); |
|||
|
|||
$file = $this->config->get('file', ini_get('error_log')); |
|||
$status = error_log($message, 3, $file); |
|||
|
|||
return compact('status', 'file'); |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\GatewayInterface; |
|||
use Overtrue\EasySms\Support\Config; |
|||
|
|||
/** |
|||
* Class Gateway. |
|||
*/ |
|||
abstract class Gateway implements GatewayInterface |
|||
{ |
|||
const DEFAULT_TIMEOUT = 5.0; |
|||
|
|||
/** |
|||
* @var \Overtrue\EasySms\Support\Config |
|||
*/ |
|||
protected $config; |
|||
|
|||
/** |
|||
* @var array |
|||
*/ |
|||
protected $options; |
|||
|
|||
/** |
|||
* @var float |
|||
*/ |
|||
protected $timeout; |
|||
|
|||
/** |
|||
* Gateway constructor. |
|||
* |
|||
* @param array $config |
|||
*/ |
|||
public function __construct(array $config) |
|||
{ |
|||
$this->config = new Config($config); |
|||
} |
|||
|
|||
/** |
|||
* Return timeout. |
|||
* |
|||
* @return int|mixed |
|||
*/ |
|||
public function getTimeout() |
|||
{ |
|||
return $this->timeout ?: $this->config->get('timeout', self::DEFAULT_TIMEOUT); |
|||
} |
|||
|
|||
/** |
|||
* Set timeout. |
|||
* |
|||
* @param int $timeout |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setTimeout($timeout) |
|||
{ |
|||
$this->timeout = floatval($timeout); |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return \Overtrue\EasySms\Support\Config |
|||
*/ |
|||
public function getConfig() |
|||
{ |
|||
return $this->config; |
|||
} |
|||
|
|||
/** |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setConfig(Config $config) |
|||
{ |
|||
$this->config = $config; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @param $options |
|||
* |
|||
* @return $this |
|||
*/ |
|||
public function setGuzzleOptions($options) |
|||
{ |
|||
$this->options = $options; |
|||
|
|||
return $this; |
|||
} |
|||
|
|||
/** |
|||
* @return array |
|||
*/ |
|||
public function getGuzzleOptions() |
|||
{ |
|||
return $this->options ?: $this->config->get('options', []); |
|||
} |
|||
|
|||
/** |
|||
* {@inheritdoc} |
|||
*/ |
|||
public function getName() |
|||
{ |
|||
return \strtolower(str_replace([__NAMESPACE__.'\\', 'Gateway'], '', \get_class($this))); |
|||
} |
|||
} |
|||
@ -0,0 +1,148 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use GuzzleHttp\Exception\RequestException; |
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Exceptions\InvalidArgumentException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
class HuaweiGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
const ENDPOINT_HOST = 'https://api.rtc.huaweicloud.com:10443'; |
|||
|
|||
const ENDPOINT_URI = '/sms/batchSendSms/v1'; |
|||
|
|||
const SUCCESS_CODE = '000000'; |
|||
|
|||
/** |
|||
* 发送信息. |
|||
* |
|||
* @param PhoneNumberInterface $to |
|||
* @param MessageInterface $message |
|||
* @param Config $config |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws GatewayErrorException |
|||
* @throws InvalidArgumentException |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$appKey = $config->get('app_key'); |
|||
$appSecret = $config->get('app_secret'); |
|||
$channels = $config->get('from'); |
|||
$statusCallback = $config->get('callback', ''); |
|||
|
|||
$endpoint = $this->getEndpoint($config); |
|||
$headers = $this->getHeaders($appKey, $appSecret); |
|||
|
|||
$templateId = $message->getTemplate($this); |
|||
$messageData = $message->getData($this); |
|||
|
|||
// 短信签名通道号码 |
|||
$from = 'default'; |
|||
if (isset($messageData['from'])) { |
|||
$from = $messageData['from']; |
|||
unset($messageData['from']); |
|||
} |
|||
$channel = isset($channels[$from]) ? $channels[$from] : ''; |
|||
|
|||
if (empty($channel)) { |
|||
throw new InvalidArgumentException("From Channel [{$from}] Not Exist"); |
|||
} |
|||
|
|||
$params = [ |
|||
'from' => $channel, |
|||
'to' => $to->getUniversalNumber(), |
|||
'templateId' => $templateId, |
|||
'templateParas' => json_encode($messageData), |
|||
'statusCallback' => $statusCallback, |
|||
]; |
|||
|
|||
try { |
|||
$result = $this->request('post', $endpoint, [ |
|||
'headers' => $headers, |
|||
'form_params' => $params, |
|||
//为防止因HTTPS证书认证失败造成API调用失败,需要先忽略证书信任问题 |
|||
'verify' => false, |
|||
]); |
|||
} catch (RequestException $e) { |
|||
$result = $this->unwrapResponse($e->getResponse()); |
|||
} |
|||
|
|||
if (self::SUCCESS_CODE != $result['code']) { |
|||
throw new GatewayErrorException($result['description'], ltrim($result['code'], 'E'), $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* 构造 Endpoint. |
|||
* |
|||
* @param Config $config |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function getEndpoint(Config $config) |
|||
{ |
|||
$endpoint = rtrim($config->get('endpoint', self::ENDPOINT_HOST), '/'); |
|||
|
|||
return $endpoint.self::ENDPOINT_URI; |
|||
} |
|||
|
|||
/** |
|||
* 获取请求 Headers 参数. |
|||
* |
|||
* @param string $appKey |
|||
* @param string $appSecret |
|||
* |
|||
* @return array |
|||
*/ |
|||
protected function getHeaders($appKey, $appSecret) |
|||
{ |
|||
return [ |
|||
'Content-Type' => 'application/x-www-form-urlencoded', |
|||
'Authorization' => 'WSSE realm="SDP",profile="UsernameToken",type="Appkey"', |
|||
'X-WSSE' => $this->buildWsseHeader($appKey, $appSecret), |
|||
]; |
|||
} |
|||
|
|||
/** |
|||
* 构造X-WSSE参数值 |
|||
* |
|||
* @param string $appKey |
|||
* @param string $appSecret |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function buildWsseHeader($appKey, $appSecret) |
|||
{ |
|||
$now = date('Y-m-d\TH:i:s\Z'); |
|||
$nonce = uniqid(); |
|||
$passwordDigest = base64_encode(hash('sha256', ($nonce.$now.$appSecret))); |
|||
|
|||
return sprintf( |
|||
'UsernameToken Username="%s",PasswordDigest="%s",Nonce="%s",Created="%s"', |
|||
$appKey, |
|||
$passwordDigest, |
|||
$nonce, |
|||
$now |
|||
); |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
/** |
|||
* Class HuaxinGateway. |
|||
* |
|||
* @see http://www.ipyy.com/help/ |
|||
*/ |
|||
class HuaxinGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
const ENDPOINT_TEMPLATE = 'http://%s/smsJson.aspx'; |
|||
|
|||
/** |
|||
* @param \Overtrue\EasySms\Contracts\PhoneNumberInterface $to |
|||
* @param \Overtrue\EasySms\Contracts\MessageInterface $message |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\GatewayErrorException ; |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$endpoint = $this->buildEndpoint($config->get('ip')); |
|||
|
|||
$result = $this->post($endpoint, [ |
|||
'userid' => $config->get('user_id'), |
|||
'account' => $config->get('account'), |
|||
'password' => $config->get('password'), |
|||
'mobile' => $to->getNumber(), |
|||
'content' => $message->getContent($this), |
|||
'sendTime' => '', |
|||
'action' => 'send', |
|||
'extno' => $config->get('ext_no'), |
|||
]); |
|||
|
|||
if ('Success' !== $result['returnstatus']) { |
|||
throw new GatewayErrorException($result['message'], 400, $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Build endpoint url. |
|||
* |
|||
* @param string $ip |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function buildEndpoint($ip) |
|||
{ |
|||
return sprintf(self::ENDPOINT_TEMPLATE, $ip); |
|||
} |
|||
} |
|||
@ -0,0 +1,77 @@ |
|||
<?php |
|||
|
|||
/* |
|||
* This file is part of the overtrue/easy-sms. |
|||
* |
|||
* (c) overtrue <i@overtrue.me> |
|||
* |
|||
* This source file is subject to the MIT license that is bundled |
|||
* with this source code in the file LICENSE. |
|||
*/ |
|||
|
|||
namespace Overtrue\EasySms\Gateways; |
|||
|
|||
use Overtrue\EasySms\Contracts\MessageInterface; |
|||
use Overtrue\EasySms\Contracts\PhoneNumberInterface; |
|||
use Overtrue\EasySms\Exceptions\GatewayErrorException; |
|||
use Overtrue\EasySms\Support\Config; |
|||
use Overtrue\EasySms\Traits\HasHttpRequest; |
|||
|
|||
/** |
|||
* Class HuyiGateway. |
|||
* |
|||
* @see http://www.ihuyi.com/api/sms.html |
|||
*/ |
|||
class HuyiGateway extends Gateway |
|||
{ |
|||
use HasHttpRequest; |
|||
|
|||
const ENDPOINT_URL = 'http://106.ihuyi.com/webservice/sms.php?method=Submit'; |
|||
|
|||
const ENDPOINT_FORMAT = 'json'; |
|||
|
|||
const SUCCESS_CODE = 2; |
|||
|
|||
/** |
|||
* @param \Overtrue\EasySms\Contracts\PhoneNumberInterface $to |
|||
* @param \Overtrue\EasySms\Contracts\MessageInterface $message |
|||
* @param \Overtrue\EasySms\Support\Config $config |
|||
* |
|||
* @return array |
|||
* |
|||
* @throws \Overtrue\EasySms\Exceptions\GatewayErrorException ; |
|||
*/ |
|||
public function send(PhoneNumberInterface $to, MessageInterface $message, Config $config) |
|||
{ |
|||
$params = [ |
|||
'account' => $config->get('api_id'), |
|||
'mobile' => $to->getIDDCode() ? \sprintf('%s %s', $to->getIDDCode(), $to->getNumber()) : $to->getNumber(), |
|||
'content' => $message->getContent($this), |
|||
'time' => time(), |
|||
'format' => self::ENDPOINT_FORMAT, |
|||
'sign' => $config->get('signature'), |
|||
]; |
|||
|
|||
$params['password'] = $this->generateSign($params); |
|||
|
|||
$result = $this->post(self::ENDPOINT_URL, $params); |
|||
|
|||
if (self::SUCCESS_CODE != $result['code']) { |
|||
throw new GatewayErrorException($result['msg'], $result['code'], $result); |
|||
} |
|||
|
|||
return $result; |
|||
} |
|||
|
|||
/** |
|||
* Generate Sign. |
|||
* |
|||
* @param array $params |
|||
* |
|||
* @return string |
|||
*/ |
|||
protected function generateSign($params) |
|||
{ |
|||
return md5($params['account'].$this->config->get('api_key').$params['mobile'].$params['content'].$params['time']); |
|||
} |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue