├── .gitignore
├── .scrutinizer.yml
├── tests
├── ClassLoader
│ ├── tInterface2.php
│ ├── tClass.php
│ ├── tTrait.php
│ ├── tInterface.php
│ ├── tAbstract.php
│ └── ClassLoaderTraitTest.php
├── ArrayTrait
│ ├── TestArrayClass.php
│ ├── ArrayTraitTest.php
│ ├── gw2api-floors-2-1.json
│ └── ByteArrayTest.php
├── Magic
│ ├── MagicTest.php
│ └── MagicContainer.php
├── Enumerable
│ ├── EnumerableContainer.php
│ └── EnumerableTest.php
└── Interfaces
│ ├── TestArrayAccessContainer.php
│ └── ArrayAccessTest.php
├── .codeclimate.yml
├── src
├── TraitException.php
├── SPL
│ ├── CountableTrait.php
│ └── SeekableIteratorTrait.php
├── Magic.php
├── EnumerableInterface.php
├── Interfaces
│ ├── ArrayAccessTrait.php
│ └── IteratorTrait.php
├── ClassLoader.php
├── ArrayHelpers
│ ├── SearchableArray.php
│ ├── DotArray.php
│ ├── ByteArray.php
│ └── ByteArrayDispenser.php
└── Enumerable.php
├── .travis.yml
├── phpunit.xml
├── composer.json
├── LICENSE
├── phpmd.xml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vendor
3 | composer.lock
4 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | filter:
2 | excluded_paths:
3 | - examples/*
4 | - tests/*
5 | - vendor/*
6 |
--------------------------------------------------------------------------------
/tests/ClassLoader/tInterface2.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits;
14 |
15 | class TraitException extends \Exception{}
16 |
--------------------------------------------------------------------------------
/tests/ClassLoader/tAbstract.php:
--------------------------------------------------------------------------------
1 | foo = $foo;
12 | }
13 |
14 | public function bar(){
15 | return $this->foo;
16 | }
17 |
18 | public function test(string $test):string{
19 | return $test;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | matrix:
4 | include:
5 | - php: 7.2
6 | - php: 7.3
7 | - php: 7.4snapshot
8 | - php: nightly
9 | allow_failures:
10 | - php: 7.4snapshot
11 | - php: nightly
12 |
13 | before_script: travis_retry composer install --no-interaction --prefer-source
14 |
15 | script: vendor/bin/phpunit --configuration phpunit.xml --coverage-clover clover.xml
16 |
17 | after_script: bash <(curl -s https://codecov.io/bash)
18 |
--------------------------------------------------------------------------------
/tests/ArrayTrait/TestArrayClass.php:
--------------------------------------------------------------------------------
1 |
8 | * @copyright 2017 Smiley
9 | * @license MIT
10 | */
11 |
12 | namespace chillerlan\TraitTest\ArrayTrait;
13 |
14 | use chillerlan\Traits\ArrayHelpers\SearchableArray;
15 |
16 | /**
17 | * Class TestArrayClass
18 | */
19 | class TestArrayClass{
20 | use SearchableArray;
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Magic/MagicTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\TraitTest\Magic;
14 |
15 | use PHPUnit\Framework\TestCase;
16 |
17 | class MagicTest extends TestCase{
18 |
19 | public function testMagic(){
20 | $magic = new MagicContainer;
21 |
22 | $magic->test = 'foo';
23 |
24 | $this->assertSame('Value: foobar', $magic->test);
25 | $this->assertnull($magic->foo);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/Enumerable/EnumerableContainer.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\TraitTest\Enumerable;
14 |
15 | use chillerlan\Traits\Enumerable;
16 | use chillerlan\Traits\EnumerableInterface;
17 |
18 | class EnumerableContainer implements EnumerableInterface{
19 | use Enumerable;
20 |
21 | public function __construct(array $data){
22 | $this->array = $data;
23 | }
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/tests/Magic/MagicContainer.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\TraitTest\Magic;
14 |
15 | use chillerlan\Traits\Magic;
16 |
17 | /**
18 | * @property $test
19 | */
20 | class MagicContainer{
21 | use Magic;
22 |
23 | protected $foo;
24 |
25 | protected function magic_get_test(){
26 | return 'Value: '.$this->foo;
27 | }
28 |
29 | protected function magic_set_test($value){
30 | $this->foo = $value.'bar';
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/tests/Interfaces/TestArrayAccessContainer.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\TraitTest\Interfaces;
14 |
15 | use chillerlan\Traits\Interfaces\ArrayAccessTrait;
16 | use chillerlan\Traits\SPL\CountableTrait;
17 | use chillerlan\Traits\SPL\SeekableIteratorTrait;
18 |
19 | class TestArrayAccessContainer implements \ArrayAccess, \Countable, \SeekableIterator{
20 | use ArrayAccessTrait, CountableTrait, SeekableIteratorTrait;
21 | }
22 |
--------------------------------------------------------------------------------
/src/SPL/CountableTrait.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits\SPL;
14 |
15 | /**
16 | * @implements \Countable
17 | *
18 | * @link http://php.net/manual/class.countable.php
19 | */
20 | trait CountableTrait{
21 |
22 | /**
23 | * @var array
24 | */
25 | protected $array = [];
26 |
27 | /**
28 | * @link http://php.net/manual/countable.count.php
29 | * @inheritdoc
30 | */
31 | public function count():int{
32 | return \count($this->array);
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 | ./src
15 |
16 |
17 |
18 |
19 | ./tests/
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chillerlan/php-traits",
3 | "description": "Some useful traits for PHP 7.2+",
4 | "homepage": "https://github.com/chillerlan/php-traits",
5 | "license": "MIT",
6 | "type": "library",
7 | "minimum-stability": "stable",
8 | "keywords": [
9 | "trait", "php7", "helper", "dotenv", "container"
10 | ],
11 | "authors": [
12 | {
13 | "name": "Smiley",
14 | "email": "smiley@chillerlan.net",
15 | "homepage": "https://github.com/codemasher"
16 | }
17 | ],
18 | "support": {
19 | "issues": "https://github.com/chillerlan/php-traits/issues",
20 | "source": "https://github.com/chillerlan/php-traits"
21 | },
22 | "require": {
23 | "php": "^7.2",
24 | "ext-json": "*"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^8.0"
28 | },
29 | "autoload": {
30 | "psr-4": {
31 | "chillerlan\\Traits\\": "src/"
32 | }
33 | },
34 | "autoload-dev": {
35 | "psr-4": {
36 | "chillerlan\\TraitTest\\": "tests/"
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/SPL/SeekableIteratorTrait.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits\SPL;
14 |
15 | use chillerlan\Traits\Interfaces\IteratorTrait;
16 | use OutOfBoundsException;
17 |
18 | /**
19 | * @extends \Iterator
20 | * @implements \SeekableIterator
21 | *
22 | * @link http://php.net/manual/class.seekableiterator.php
23 | */
24 | trait SeekableIteratorTrait{
25 | use IteratorTrait;
26 |
27 | /**
28 | * @link http://php.net/manual/seekableiterator.seek.php
29 | * @inheritdoc
30 | */
31 | public function seek($pos):void{
32 | $this->rewind();
33 |
34 | for( ; $this->offset < $pos; ){
35 |
36 | if(!\next($this->array)) {
37 | throw new OutOfBoundsException('invalid seek position: '.$pos);
38 | }
39 |
40 | $this->offset++;
41 | }
42 |
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Smiley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/phpmd.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 | codemasher/php-traits PMD ruleset
8 | */examples/*
9 | */tests/*
10 |
11 |
12 |
13 |
14 | 1
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/Magic.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits;
14 |
15 | /**
16 | * A Container that turns methods into magic properties
17 | */
18 | trait Magic{
19 |
20 | /**
21 | * @param string $name
22 | *
23 | * @return mixed|null
24 | */
25 | public function __get(string $name){
26 | return $this->get($name);
27 | }
28 |
29 | /**
30 | * @param string $name
31 | * @param mixed $value
32 | *
33 | * @return void
34 | */
35 | public function __set(string $name, $value):void{
36 | $this->set($name, $value);
37 | }
38 |
39 | /**
40 | * @param string $name
41 | *
42 | * @return mixed|null
43 | */
44 | private function get(string $name){
45 | $method = 'magic_get_'.$name;
46 |
47 | return \method_exists($this, $method) ? $this->$method() : null;
48 | }
49 |
50 | /**
51 | * @param string $name
52 | * @param $value
53 | *
54 | * @return void
55 | */
56 | private function set(string $name, $value):void{
57 | $method = 'magic_set_'.$name;
58 |
59 | if(\method_exists($this, $method)){
60 | $this->$method($value);
61 | }
62 |
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Interfaces/ArrayAccessTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\TraitTest\Interfaces;
14 |
15 | use OutOfBoundsException;
16 | use PHPUnit\Framework\TestCase;
17 |
18 | class ArrayAccessTest extends TestCase{
19 |
20 | public function testInstance(){
21 | $x = new TestArrayAccessContainer;
22 |
23 | foreach(range(0, 68) as $k){
24 | $x[$k] = ['id' => $k, 'hash' => md5($k)];
25 | }
26 |
27 | $x[] = ['id' => 69, 'hash' => md5(69)]; // coverage
28 |
29 | $this->assertCount(70, $x); // \Countable
30 |
31 | $x->seek(69);
32 |
33 | $this->assertSame(md5(69), $x->current()['hash']);
34 |
35 | foreach($x as $k => $v){ // \Iterator
36 | if(isset($x[$k])){ // coverage
37 | $this->assertSame(md5($k), $v['hash']);
38 | $this->assertSame($v, $x[$k]); // coverage
39 | unset($x[$k]); // coverage
40 | }
41 | }
42 |
43 | $this->assertCount(0, $x);
44 | }
45 |
46 | public function testSeekInvalidPos(){
47 | $this->expectException(OutOfBoundsException::class);
48 | $this->expectExceptionMessage('invalid seek position: 69');
49 |
50 | $x = new TestArrayAccessContainer;
51 | $x->seek(69);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/EnumerableInterface.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2018 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits;
14 |
15 | interface EnumerableInterface{
16 |
17 | /**
18 | * @return array
19 | */
20 | public function __toArray():array;
21 |
22 | /**
23 | * @param callable $callback
24 | *
25 | * @return mixed
26 | */
27 | public function __each($callback);
28 |
29 | /**
30 | * @param callable $callback
31 | *
32 | * @return array
33 | */
34 | public function __map($callback):array;
35 |
36 | /**
37 | * @return \chillerlan\Traits\EnumerableInterface
38 | */
39 | public function __reverse():EnumerableInterface;
40 |
41 | /**
42 | * @return mixed
43 | */
44 | public function __first();
45 |
46 | /**
47 | * @return mixed
48 | */
49 | public function __last();
50 |
51 | /**
52 | * @return \chillerlan\Traits\EnumerableInterface
53 | */
54 | public function __clear():EnumerableInterface;
55 |
56 | /**
57 | * @return string
58 | */
59 | public function __inspect():string;
60 |
61 | /**
62 | * @param callable $callback
63 | *
64 | * @return array
65 | */
66 | public function __findAll($callback):array;
67 |
68 | /**
69 | * @param callable $callback
70 | *
71 | * @return array
72 | */
73 | public function __reject($callback):array;
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/Interfaces/ArrayAccessTrait.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits\Interfaces;
14 |
15 | /**
16 | * @implements ArrayAccess
17 | *
18 | * @link http://php.net/manual/class.arrayaccess.php
19 | */
20 | trait ArrayAccessTrait{
21 |
22 | /**
23 | * @var array
24 | */
25 | protected $array = [];
26 |
27 | /**
28 | * @var int
29 | */
30 | protected $offset = 0;
31 |
32 | /**
33 | * @link http://php.net/manual/arrayaccess.offsetexists.php
34 | * @inheritdoc
35 | */
36 | public function offsetExists($offset):bool{
37 | return \array_key_exists($offset, $this->array);
38 | }
39 |
40 | /**
41 | * @link http://php.net/manual/arrayaccess.offsetget.php
42 | * @inheritdoc
43 | */
44 | public function offsetGet($offset){
45 | return $this->array[$offset] ?? null;
46 | }
47 |
48 | /**
49 | * @link http://php.net/manual/arrayaccess.offsetset.php
50 | * @inheritdoc
51 | */
52 | public function offsetSet($offset, $value):void{
53 |
54 | $offset !== null
55 | ? $this->array[$offset] = $value
56 | : $this->array[] = $value;
57 | }
58 |
59 | /**
60 | * @link http://php.net/manual/arrayaccess.offsetunset.php
61 | * @inheritdoc
62 | */
63 | public function offsetUnset($offset):void{
64 | unset($this->array[$offset]);
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/Interfaces/IteratorTrait.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits\Interfaces;
14 |
15 | /**
16 | * @extends \Traversable
17 | * @implements \Iterator
18 | *
19 | * @link http://php.net/manual/class.traversable.php
20 | */
21 | trait IteratorTrait{
22 |
23 | /**
24 | * @var array
25 | */
26 | protected $array = [];
27 |
28 | /**
29 | * @var int
30 | */
31 | protected $offset = 0;
32 |
33 | /**
34 | * @link http://php.net/manual/iterator.current.php
35 | * @inheritdoc
36 | */
37 | public function current(){
38 | return $this->array[$this->offset] ?? null;
39 | }
40 |
41 | /**
42 | * @link http://php.net/manual/iterator.next.php
43 | * @inheritdoc
44 | */
45 | public function next():void{
46 | $this->offset++;
47 | }
48 |
49 | /**
50 | * @link http://php.net/manual/iterator.key.php
51 | * @inheritdoc
52 | */
53 | public function key(){
54 | return $this->offset;
55 | }
56 |
57 | /**
58 | * @link http://php.net/manual/iterator.valid.php
59 | * @inheritdoc
60 | */
61 | public function valid():bool{
62 | return \array_key_exists($this->offset, $this->array);
63 | }
64 |
65 | /**
66 | * @link http://php.net/manual/iterator.rewind.php
67 | * @inheritdoc
68 | */
69 | public function rewind():void{
70 | $this->offset = 0;
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/tests/ArrayTrait/ArrayTraitTest.php:
--------------------------------------------------------------------------------
1 |
8 | * @copyright 2017 Smiley
9 | * @license MIT
10 | */
11 |
12 | namespace chillerlan\TraitTest\ArrayTrait;
13 |
14 | use PHPUnit\Framework\TestCase;
15 |
16 | class SearchableArrayTest extends TestCase{
17 |
18 | public function testSearchableArray(){
19 | // https://api.guildwars2.com/v2/continents/2/floors/1
20 | $array = new TestArrayClass(json_decode(file_get_contents(__DIR__.'/gw2api-floors-2-1.json'), true));
21 |
22 | $k = 'regions.7.maps.38.points_of_interest.990.name';
23 | $v = 'Stonemist Keep';
24 |
25 | // coverage - first level
26 | $this->assertSame('World vs. World', $array->get('regions')[7]['name']);
27 | $this->assertTrue($array->in('regions'));
28 |
29 | $this->assertSame($v, $array->get($k));
30 | $this->assertSame($v, $array->searchByKey($k)); // RecursiveIterator
31 | $this->assertSame([$k => $v], $array->searchByValue($v)); // RecursiveIterator
32 |
33 | $this->assertNull($array->get($k.'.foo'));
34 | $this->assertNull($array->searchByKey($k.'.foo')); // RecursiveIterator
35 |
36 | $this->assertTrue($array->in($k));
37 | $this->assertTrue($array->isset($k)); // RecursiveIterator
38 |
39 | $this->assertFalse($array->in($k.'.foo'));
40 | $this->assertFalse($array->isset($k.'.foo')); // RecursiveIterator
41 |
42 | $array->set($k, 'foo');
43 | $this->assertSame('foo', $array->get($k));
44 |
45 | }
46 |
47 | public function testSet(){
48 | $array = new TestArrayClass;
49 |
50 | $this->assertFalse($array->set('', null)->in('nothing'));
51 |
52 | $array->set('foo.bar', 'whatever');
53 |
54 | $this->assertSame('whatever', $array->get('foo.bar'));
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/src/ClassLoader.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits;
14 |
15 | use Exception, ReflectionClass;
16 |
17 | trait ClassLoader{
18 |
19 | /**
20 | * Instances an object of $class/$type with an arbitrary number of $params
21 | *
22 | * @param string $class class FQCN
23 | * @param string $type class/parent/interface FQCN
24 | *
25 | * @param mixed $params [optional] the following arguments will be passed to the $class constructor
26 | *
27 | * @return mixed of type $type
28 | * @throws \Exception
29 | */
30 | public function loadClass(string $class, string $type = null, ...$params){
31 | $type = $type ?? $class;
32 |
33 | try{
34 | $reflectionClass = new ReflectionClass($class);
35 | $reflectionType = new ReflectionClass($type);
36 | }
37 | catch(Exception $e){
38 | throw new TraitException('ClassLoader: '.$e->getMessage());
39 | }
40 |
41 |
42 | if($reflectionType->isTrait()){
43 | throw new TraitException($class.' cannot be an instance of trait '.$type);
44 | }
45 |
46 | if($reflectionClass->isAbstract()){
47 | throw new TraitException('cannot instance abstract class '.$class);
48 | }
49 |
50 | if($reflectionClass->isTrait()){
51 | throw new TraitException('cannot instance trait '.$class);
52 | }
53 |
54 | if($class !== $type){
55 |
56 | if($reflectionType->isInterface() && !$reflectionClass->implementsInterface($type)){
57 | throw new TraitException($class.' does not implement '.$type);
58 | }
59 | elseif(!$reflectionClass->isSubclassOf($type)) {
60 | throw new TraitException($class.' does not inherit '.$type);
61 | }
62 |
63 | }
64 |
65 | try{
66 | $object = $reflectionClass->newInstanceArgs($params);
67 |
68 | if(!$object instanceof $type){
69 | throw new TraitException('how did u even get here?'); // @codeCoverageIgnore
70 | }
71 |
72 | return $object;
73 | }
74 | // @codeCoverageIgnoreStart
75 | // here be dragons
76 | catch(Exception $e){
77 | throw new TraitException('ClassLoader: '.$e->getMessage());
78 | }
79 | // @codeCoverageIgnoreEnd
80 |
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/src/ArrayHelpers/SearchableArray.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits\ArrayHelpers;
14 |
15 | use ArrayIterator, ArrayObject, RecursiveArrayIterator, RecursiveIteratorIterator, Traversable;
16 |
17 | trait SearchableArray{
18 | use DotArray;
19 |
20 | /**
21 | * @var \IteratorIterator|\RecursiveIteratorIterator
22 | */
23 | protected $iterator;
24 |
25 | /**
26 | * SearchableArray constructor.
27 | *
28 | * @param array|object|\Traversable|\ArrayIterator|\ArrayObject|null $array
29 | */
30 | public function __construct($array = null){
31 |
32 | if(($array instanceof ArrayObject) || ($array instanceof ArrayIterator)){
33 | $this->array = $array->getArrayCopy(); // @codeCoverageIgnore
34 | }
35 | elseif($array instanceof Traversable){
36 | $this->array = \iterator_to_array($array); // @codeCoverageIgnore
37 | }
38 | // yields unexpected results with DotArray
39 | elseif(\gettype($array) === 'object'){
40 | $this->array = \get_object_vars($array); // @codeCoverageIgnore
41 | }
42 | elseif(\is_array($array)){
43 | $this->array = $array;
44 | }
45 |
46 | $this->iterator = new RecursiveIteratorIterator(
47 | new RecursiveArrayIterator($this->array),
48 | RecursiveIteratorIterator::SELF_FIRST
49 | );
50 | }
51 |
52 | /**
53 | * @param string $dotKey
54 | *
55 | * @return mixed
56 | */
57 | public function searchByKey(string $dotKey){
58 |
59 | foreach($this->iterator as $v){
60 |
61 | if($this->getPath() === $dotKey){
62 | return $v;
63 | }
64 |
65 | }
66 |
67 | return null;
68 | }
69 |
70 | /**
71 | * @param mixed $value
72 | *
73 | * @return array
74 | */
75 | public function searchByValue($value):array{
76 |
77 | $matches = [];
78 |
79 | foreach($this->iterator as $v){
80 |
81 | if($v === $value){
82 | $matches[$this->getPath()] = $value;
83 | }
84 |
85 | }
86 |
87 | return $matches;
88 | }
89 |
90 | /**
91 | * @param string $dotKey
92 | *
93 | * @return bool
94 | */
95 | public function isset(string $dotKey):bool{
96 |
97 | foreach($this->iterator as $v){
98 |
99 | if($this->getPath() === $dotKey){
100 | return true;
101 | }
102 |
103 | }
104 |
105 | return false;
106 | }
107 |
108 | /**
109 | * @return string
110 | */
111 | private function getPath():string{
112 | return \implode('.', \array_map(function(int $depth):string {
113 | return $this->iterator->getSubIterator($depth)->key();
114 | }, \range(0, $this->iterator->getDepth())));
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/tests/ClassLoader/ClassLoaderTraitTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\TraitTest\ClassLoader;
14 |
15 | use chillerlan\Traits\ClassLoader;
16 | use chillerlan\Traits\TraitException;
17 | use PHPUnit\Framework\TestCase;
18 |
19 | class ClassLoaderTraitTest extends TestCase{
20 | use ClassLoader;
21 |
22 | public function testLoadClass1(){
23 | $this->assertInstanceOf(tClass::class, $this->loadClass(tClass::class));
24 | $this->assertInstanceOf(tClass::class, $this->loadClass(tClass::class, tClass::class));
25 | $this->assertInstanceOf(tInterface::class, $this->loadClass(tClass::class, tInterface::class));
26 | $this->assertInstanceOf(tAbstract::class, $this->loadClass(tClass::class, tAbstract::class));
27 | }
28 |
29 | public function testLoadClass2(){
30 | $obj = $this->loadClass(tClass::class, tInterface::class);
31 | $this->assertSame('foo', $obj->test('foo'));
32 | $this->assertSame('bar', $obj->testTrait('bar'));
33 | }
34 |
35 | public function testLoadClass3(){
36 | $obj = $this->loadClass(tClass::class, tInterface::class, 'whatever');
37 |
38 | $this->assertSame('whatever', $obj->bar());
39 | }
40 |
41 | public function testLoadClassExceptionA1(){
42 | $this->expectException(TraitException::class);
43 | $this->expectExceptionMessage('\whatever\foo does not exist');
44 |
45 | $this->loadClass('\\whatever\\foo', tInterface::class);
46 | }
47 |
48 | public function testLoadClassExceptionA2(){
49 | $this->expectException(TraitException::class);
50 | $this->expectExceptionMessage('\whatever\bar does not exist');
51 |
52 | $this->loadClass(tClass::class, '\\whatever\\bar');
53 | }
54 |
55 | public function testLoadClassExceptionB(){
56 | $this->expectException(TraitException::class);
57 | $this->expectExceptionMessage('cannot be an instance of trait');
58 |
59 | $this->loadClass(tClass::class, tTrait::class);
60 | }
61 |
62 | public function testLoadClassExceptionC(){
63 | $this->expectException(TraitException::class);
64 | $this->expectExceptionMessage('cannot instance abstract class');
65 |
66 | $this->loadClass(tAbstract::class, tInterface::class);
67 | }
68 |
69 | public function testLoadClassExceptionD(){
70 | $this->expectException(TraitException::class);
71 | $this->expectExceptionMessage('cannot instance trait');
72 |
73 | $this->loadClass(tTrait::class, tInterface::class);
74 | }
75 |
76 | public function testLoadClassExceptionE1(){
77 | $this->expectException(TraitException::class);
78 | $this->expectExceptionMessage('does not implement');
79 |
80 | $this->loadClass(tClass::class, tInterface2::class);
81 | }
82 |
83 | public function testLoadClassExceptionE2(){
84 | $this->expectException(TraitException::class);
85 | $this->expectExceptionMessage('does not inherit');
86 |
87 | $this->loadClass(tClass::class, \stdClass::class);
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/ArrayHelpers/DotArray.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits\ArrayHelpers;
14 |
15 | /**
16 | * @link https://github.com/laravel/framework/blob/5.4/src/Illuminate/Support/Arr.php
17 | */
18 | trait DotArray{
19 |
20 | /**
21 | * @var array
22 | */
23 | protected $array = [];
24 |
25 | /**
26 | * Checks if $key isset in $array using dot notation and returns it on success.
27 | *
28 | * @param string $dotKey the key to search
29 | * @param mixed $default [optional] a default value in case the key isn't being found
30 | *
31 | * @return mixed returns $array[$key], $default otherwise.
32 | */
33 | public function get(string $dotKey, $default = null){
34 |
35 | if(isset($this->array[$dotKey])){
36 | return $this->array[$dotKey];
37 | }
38 |
39 | $array = &$this->array;
40 |
41 | foreach(\explode('.', $dotKey) as $segment){
42 |
43 | if(!\is_array($array) || !\array_key_exists($segment, $array)){
44 | return $default;
45 | }
46 |
47 | $array = &$array[$segment];
48 | }
49 |
50 | return $array;
51 | }
52 |
53 | /**
54 | * Checks if $key exists in $array using dot notation and returns it on success
55 | *
56 | * @param string $dotKey the key to search
57 | *
58 | * @return bool
59 | */
60 | public function in(string $dotKey):bool{
61 |
62 | if(empty($this->array)){
63 | return false;
64 | }
65 |
66 | if(\array_key_exists($dotKey, $this->array)){
67 | return true;
68 | }
69 |
70 | $array = &$this->array;
71 |
72 | foreach(\explode('.', $dotKey) as $segment){
73 |
74 | if(!\is_array($array) || !\array_key_exists($segment, $array)){
75 | return false;
76 | }
77 |
78 | $array = &$array[$segment];
79 | }
80 |
81 | return true;
82 | }
83 |
84 | /**
85 | * Sets $key in $array using dot notation
86 | *
87 | * If no key is given to the method, the entire array will be replaced.
88 | *
89 | * @param string $dotKey
90 | * @param mixed $value
91 | *
92 | * @return \chillerlan\Traits\ArrayHelpers\DotArray
93 | */
94 | public function set(string $dotKey, $value){
95 |
96 | if(empty($dotKey)){
97 | $this->array = $value;
98 |
99 | return $this;
100 | }
101 |
102 | $array = &$this->array;
103 | $keys = \explode('.', $dotKey);
104 |
105 | while(\count($keys) > 1){
106 | $dotKey = \array_shift($keys);
107 |
108 | // If the key doesn't exist at this depth, we will just create an empty array
109 | // to hold the next value, allowing us to create the arrays to hold final
110 | // values at the correct depth. Then we'll keep digging into the array.
111 | if(!isset($array[$dotKey]) || !\is_array($array[$dotKey])){
112 | $array[$dotKey] = [];
113 | }
114 |
115 | $array = &$array[$dotKey];
116 | }
117 |
118 | $array[\array_shift($keys)] = $value;
119 |
120 | return $this;
121 | }
122 |
123 | }
124 |
--------------------------------------------------------------------------------
/src/ArrayHelpers/ByteArray.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits\ArrayHelpers;
14 |
15 | use ReflectionClass, SplFixedArray;
16 |
17 | /**
18 | * @extends \SplFixedArray
19 | */
20 | class ByteArray extends SplFixedArray{
21 |
22 | /**
23 | * @return string
24 | */
25 | public function toString():string{
26 | return $this->map('chr');
27 | }
28 |
29 | /**
30 | * @return string
31 | */
32 | public function toHex():string{
33 | return $this->map(function($v){
34 | return \str_pad(\dechex($v), '2', '0', \STR_PAD_LEFT);
35 | });
36 | }
37 |
38 | /**
39 | * @return string
40 | */
41 | public function toJSON():string{
42 | return \json_encode($this->toArray());
43 | }
44 |
45 | /**
46 | * @return string
47 | */
48 | public function toBase64():string{
49 | return \base64_encode($this->toString());
50 | }
51 |
52 | /**
53 | * @return string
54 | */
55 | public function toBin():string{
56 | return $this->map(function($v){
57 | return \str_pad(\decbin($v), '8', '0', \STR_PAD_LEFT);
58 | });
59 | }
60 |
61 | /**
62 | * @param callable $m
63 | *
64 | * @return string
65 | */
66 | public function map(callable $m):string{
67 | return \implode('', \array_map($m, $this->toArray()));
68 | }
69 |
70 | /**
71 | * @param \SplFixedArray $src
72 | * @param int $length
73 | * @param int|null $offset
74 | * @param int|null $srcOffset
75 | *
76 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray
77 | */
78 | public function copyFrom(SplFixedArray $src, int $length = null, int $offset = null, int $srcOffset = null):ByteArray{
79 | $length = $length ?? $src->count();
80 | $offset = $offset ?? $length;
81 | $srcOffset = $srcOffset ?? 0;
82 |
83 | $diff = $offset + $length;
84 |
85 | if($diff > $this->count()){
86 | $this->setSize($diff);
87 | }
88 |
89 | for($i = 0; $i < $length; $i++){
90 | $this[$i + $offset] = $src[$i + $srcOffset];
91 | }
92 |
93 | return $this;
94 | }
95 |
96 | /**
97 | * @param int $offset
98 | * @param int|null $length
99 | *
100 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray
101 | */
102 | public function slice(int $offset, int $length = null):ByteArray{
103 |
104 | // keep an extended class
105 | /** @var \chillerlan\Traits\ArrayHelpers\ByteArray $slice */
106 | $slice = (new ReflectionClass($this))->newInstanceArgs([$length ?? ($this->count() - $offset)]);
107 |
108 | foreach($slice as $i => $_){
109 | $slice[$i] = $this[$offset + $i];
110 | }
111 |
112 | return $slice;
113 | }
114 |
115 | /**
116 | * @param \SplFixedArray $array
117 | *
118 | * @return bool
119 | */
120 | public function equal(SplFixedArray $array):bool{
121 |
122 | if($this->count() !== $array->count()){
123 | return false;
124 | }
125 |
126 | $diff = 0;
127 |
128 | foreach($this as $k => $v){
129 | $diff |= $v ^ $array[$k];
130 | }
131 |
132 | $diff = ($diff - 1) >> 31;
133 |
134 | return ($diff & 1) === 1;
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/tests/Enumerable/EnumerableTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\TraitTest\Enumerable;
14 |
15 | use chillerlan\Traits\TraitException;
16 | use PHPUnit\Framework\TestCase;
17 |
18 | class EnumerableTest extends TestCase{
19 |
20 | protected $array = [1, 'two', 3, 'four', 5];
21 |
22 | /**
23 | * @var \chillerlan\TraitTest\Enumerable\EnumerableContainer
24 | */
25 | protected $enumerable;
26 |
27 | protected function setUp():void{
28 | $this->enumerable = new EnumerableContainer($this->array);
29 | }
30 |
31 | public function testMap(){
32 |
33 | $arr = $this->enumerable
34 | ->__map(function($v, $k){
35 | return $v;
36 | })
37 | ;
38 |
39 | $this->assertSame($arr, $this->enumerable->__toArray());
40 | }
41 |
42 | public function testReverse(){
43 | $arr = $this->enumerable
44 | ->__reverse()
45 | ->__toArray()
46 | ;
47 |
48 | $this->assertSame(array_reverse($this->array), $arr);
49 | }
50 |
51 | public function testEach(){
52 |
53 | $this->enumerable
54 | ->__each(function($v, $k){
55 | $this->assertSame($this->array[$k], $v);
56 | })
57 | ;
58 |
59 | }
60 |
61 | public function testFirst(){
62 | $this->assertSame(1, $this->enumerable->__first());
63 | }
64 |
65 | public function testLast(){
66 | $this->assertSame(5, $this->enumerable->__last());
67 | }
68 |
69 | public function testClear(){
70 |
71 | $arr = $this->enumerable
72 | ->__clear()
73 | ->__toArray()
74 | ;
75 |
76 | $this->assertSame([], $arr);
77 | }
78 |
79 | public function testInspect(){
80 |
81 | $expected = 'Array
82 | (
83 | [0] => 1
84 | [1] => two
85 | [2] => 3
86 | [3] => four
87 | [4] => 5
88 | )
89 | ';
90 |
91 | $expected = str_replace("\r", '', $expected); // will we ever settle on ONE line ending that is \n???
92 |
93 | $this->assertSame($expected, $this->enumerable->__inspect());
94 | }
95 |
96 | public function testFindAll(){
97 | $enum = new EnumerableContainer([1, 'two', 3, 'four', 5]);
98 |
99 | $arr = $enum->__findAll(function($e, $i){
100 | return is_string($e);
101 | });
102 |
103 | $this->assertSame(['two', 'four'], $arr);
104 | }
105 |
106 | public function testReject(){
107 | $enum = new EnumerableContainer([1, 'two', 3, 'four', 5]);
108 |
109 | $arr = $enum->__reject(function($e, $i){
110 | return is_string($e);
111 | });
112 |
113 | $this->assertSame([1, 3, 5], $arr);
114 | }
115 |
116 | public function testMapInvalidCallback(){
117 | $this->expectException(TraitException::class);
118 | $this->expectExceptionMessage('invalid callback');
119 |
120 | (new EnumerableContainer([]))->__map('foo');
121 | }
122 |
123 | public function testFindAllInvalidCallback(){
124 | $this->expectException(TraitException::class);
125 | $this->expectExceptionMessage('invalid callback');
126 |
127 | (new EnumerableContainer([]))->__findAll('foo');
128 | }
129 |
130 | public function testRejectInvalidCallback(){
131 | $this->expectException(TraitException::class);
132 | $this->expectExceptionMessage('invalid callback');
133 |
134 | (new EnumerableContainer([]))->__reject('foo');
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/src/Enumerable.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits;
14 |
15 | /**
16 | * @link http://api.prototypejs.org/language/Enumerable/
17 | */
18 | trait Enumerable{
19 |
20 | /**
21 | * @var array
22 | */
23 | protected $array = [];
24 |
25 | /**
26 | * @var int
27 | */
28 | protected $offset = 0;
29 |
30 | /**
31 | * @link http://api.prototypejs.org/language/Enumerable/prototype/toArray/
32 | *
33 | * @return array
34 | *
35 | * @codeCoverageIgnore
36 | */
37 | public function __toArray():array {
38 | return $this->array;
39 | }
40 |
41 | /**
42 | * @link http://api.prototypejs.org/language/Enumerable/prototype/each/
43 | *
44 | * @param callable $callback
45 | *
46 | * @return $this
47 | */
48 | public function __each($callback){
49 | $this->__map($callback);
50 |
51 | return $this;
52 | }
53 |
54 | /**
55 | * @link http://api.prototypejs.org/language/Enumerable/prototype/collect/
56 | * @link http://api.prototypejs.org/language/Enumerable/prototype/map/
57 | *
58 | * @param callable $callback
59 | *
60 | * @return array
61 | * @throws \chillerlan\Traits\TraitException
62 | */
63 | public function __map($callback):array {
64 |
65 | if(!\is_callable($callback)){
66 | throw new TraitException('invalid callback');
67 | }
68 |
69 | $return = [];
70 |
71 | foreach($this->array as $index => $element){
72 | $return[$index] = \call_user_func_array($callback, [$element, $index]);
73 | }
74 |
75 | return $return;
76 | }
77 |
78 | /**
79 | * @link http://api.prototypejs.org/language/Array/prototype/reverse/
80 | *
81 | * @return \chillerlan\Traits\EnumerableInterface
82 | */
83 | public function __reverse():EnumerableInterface{
84 | $this->array = \array_reverse($this->array);
85 | $this->offset = 0;
86 |
87 | return $this;
88 | }
89 |
90 | /**
91 | * @return mixed
92 | */
93 | public function __first(){
94 | return $this->array[0] ?? null;
95 | }
96 |
97 | /**
98 | * @return mixed
99 | */
100 | public function __last(){
101 | return $this->array[\count($this->array) - 1] ?? null;
102 | }
103 |
104 | /**
105 | * @return \chillerlan\Traits\EnumerableInterface
106 | */
107 | public function __clear():EnumerableInterface{
108 | $this->array = [];
109 |
110 | return $this;
111 | }
112 |
113 | /**
114 | * @link http://api.prototypejs.org/language/Array/prototype/inspect/
115 | *
116 | * @return string
117 | */
118 | public function __inspect():string {
119 | return \print_r($this->array, true);
120 | }
121 |
122 | /**
123 | * @link http://api.prototypejs.org/language/Enumerable/prototype/findAll/
124 | *
125 | * @param callable $callback
126 | *
127 | * @return array
128 | * @throws \chillerlan\Traits\TraitException
129 | */
130 | public function __findAll($callback):array{
131 |
132 | if(!\is_callable($callback)){
133 | throw new TraitException('invalid callback');
134 | }
135 |
136 | $return = [];
137 |
138 | foreach($this->array as $index => $element){
139 |
140 | if(\call_user_func_array($callback, [$element, $index]) === true){
141 | $return[] = $element;
142 | }
143 |
144 | }
145 |
146 | return $return;
147 | }
148 |
149 | /**
150 | * @link http://api.prototypejs.org/language/Enumerable/prototype/reject/
151 | *
152 | * @param callable $callback
153 | *
154 | * @return array
155 | * @throws \chillerlan\Traits\TraitException
156 | */
157 | public function __reject($callback):array{
158 |
159 | if(!\is_callable($callback)){
160 | throw new TraitException('invalid callback');
161 | }
162 |
163 | $return = [];
164 |
165 | foreach($this->array as $index => $element){
166 |
167 | if(\call_user_func_array($callback, [$element, $index]) !== true){
168 | $return[] = $element;
169 | }
170 |
171 | }
172 |
173 | return $return;
174 | }
175 |
176 | }
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # chillerlan/php-traits
2 |
3 | A collection of (more or less) useful traits for PHP7.2+
4 |
5 | [![version][packagist-badge]][packagist]
6 | [![license][license-badge]][license]
7 | [![Travis][travis-badge]][travis]
8 | [![Coverage][coverage-badge]][coverage]
9 | [![Scrunitizer][scrutinizer-badge]][scrutinizer]
10 | [![Packagist downloads][downloads-badge]][downloads]
11 | [![PayPal donate][donate-badge]][donate]
12 |
13 | [packagist-badge]: https://img.shields.io/packagist/v/chillerlan/php-traits.svg?style=flat-square
14 | [packagist]: https://packagist.org/packages/chillerlan/php-traits
15 | [license-badge]: https://img.shields.io/github/license/chillerlan/php-traits.svg?style=flat-square
16 | [license]: https://github.com/chillerlan/php-traits/blob/master/LICENSE
17 | [travis-badge]: https://img.shields.io/travis/chillerlan/php-traits.svg?style=flat-square
18 | [travis]: https://travis-ci.org/chillerlan/php-traits
19 | [coverage-badge]: https://img.shields.io/codecov/c/github/chillerlan/php-traits.svg?style=flat-square
20 | [coverage]: https://codecov.io/github/chillerlan/php-traits
21 | [scrutinizer-badge]: https://img.shields.io/scrutinizer/g/chillerlan/php-traits.svg?style=flat-square
22 | [scrutinizer]: https://scrutinizer-ci.com/g/chillerlan/php-traits
23 | [downloads-badge]: https://img.shields.io/packagist/dt/chillerlan/php-traits.svg?style=flat-square
24 | [downloads]: https://packagist.org/packages/chillerlan/php-traits/stats
25 | [donate-badge]: https://img.shields.io/badge/donate-paypal-ff33aa.svg?style=flat-square
26 | [donate]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=WLYUNAT9ZTJZ4
27 |
28 | ## Features
29 | - `ClassLoader` - invokes objects of a given class and interface/type with an arbitrary count of constructor arguments
30 | - `Magic` - turns methods into magic properties
31 | - `Enumerable` - provides some of [prototype's enumerable methods](http://api.prototypejs.org/language/Enumerable/), implements `EnumerableInterface`
32 | - `ArrayHelpers`
33 | - `ByteArray` - useful for byte/bit-flipping purposes, extends [`SplFixedArray`](http://php.net/manual/class.splfixedarray.php)
34 | - `ByteArrayDispenser` - creates `ByteArray` from several data types (hex, base64, binary, json etc.)
35 | - `DotArray` - adds dot key notation functionality
36 | - `SearchableArray` - deep search arrays using [`RecursiveIteratorIterator`](http://php.net/manual/class.recursiveiteratoriterator.php)
37 | - `Interfaces`
38 | - `ArrayAccessTrait` - implements [`ArrayAccess`](http://php.net/manual/class.arrayaccess.php)
39 | - `IteratorTrait` - implements [`Iterator`](http://php.net/manual/class.iterator.php)
40 | - `SPL`
41 | - `CountableTrait` - implements [`Countable`](http://php.net/manual/class.countable.php)
42 | - `SeekableIteratorTrait` - implements [`SeekableIterator`](http://php.net/manual/class.seekableiterator.php)
43 |
44 | ## Documentation
45 |
46 | ### Installation
47 | **requires [composer](https://getcomposer.org)**
48 |
49 | *composer.json* (note: replace `dev-master` with a version boundary)
50 | ```json
51 | {
52 | "require": {
53 | "php": "^7.2",
54 | "chillerlan/php-traits": "dev-master"
55 | }
56 | }
57 | ```
58 |
59 | #### Manual installation
60 | Download the desired version of the package from [master](https://github.com/chillerlan/php-traits/archive/master.zip) or
61 | [release](https://github.com/chillerlan/php-traits/releases) and extract the contents to your project folder. After that:
62 | - run `composer install` to install the required dependencies and generate `/vendor/autoload.php`.
63 | - if you use a custom autoloader, point the namespace `chillerlan\Traits` to the folder `src` of the package
64 |
65 | Profit!
66 |
67 | ### Usage
68 |
69 | #### `ClassLoader`
70 | Simple usage:
71 | ```php
72 | class MyClass{
73 | use ClassLoader;
74 |
75 | protected function doStuff(string $class){
76 | $obj = $this->loadClass(__NAMESPACE__.'\\Whatever\\'.$class);
77 |
78 | // do stuff
79 | }
80 | }
81 | ```
82 |
83 | Let's assume we have several classes that implement the same interface, but their constructors have different parameter counts, like so:
84 | ```php
85 | class SomeClass implements MyInterface{
86 | public funtion __construct($param_foo){}
87 | }
88 |
89 | class OtherClass implements MyInterface{
90 | public funtion __construct($param_foo, $param_bar){}
91 | }
92 | ```
93 |
94 | Initialize an object based on a selction
95 |
96 | ```php
97 | class MyClass{
98 | use ClassLoader;
99 |
100 | protected $classes = [
101 | 'foo' => SomeClass::class,
102 | 'bar' => OtherClass::class
103 | ];
104 |
105 | protected funtion initInterface(string $whatever, $foo, $bar = null):MyInterface{
106 |
107 | foreach($this->classes as $what => $class){
108 | if($whatever === $what){
109 | return $this->loadClass($class, MyInterface::class, $foo, $bar);
110 | }
111 | }
112 |
113 | }
114 | }
115 | ```
116 |
117 |
118 | #### `Magic`
119 | `Magic` allows to access internal methods like as properties.
120 | ```php
121 | class MyMagicContainer{
122 | use Magic;
123 |
124 | protected $foo;
125 |
126 | protected function magic_get_foo(){
127 | // do whatever...
128 |
129 | return 'foo: '.$this->foo;
130 | }
131 |
132 | protected function magic_set_foo($value){
133 | // do stuff with $value
134 | // ...
135 |
136 | $this->foo = $value.'bar';
137 | }
138 | }
139 | ```
140 |
141 | ```php
142 | $magic = new MyMagicContainer;
143 |
144 | $magic->foo = 'foo';
145 |
146 | var_dump($magic->foo); // -> foo: foobar
147 |
148 | ```
149 |
150 | #### `Enumerable`
151 | ```php
152 | class MyEnumerableContainer implements EnumerableInterface{
153 | use Enumerable;
154 |
155 | public function __construct(array $data){
156 | $this->array = $data;
157 | }
158 | }
159 | ```
160 |
161 | ```php
162 | $enum = new MyEnumerableContainer($data);
163 |
164 | $enum
165 | ->__each(function($value, $index){
166 | // do stuff
167 |
168 | $this->array[$index] = $stuff;
169 | })
170 | ->__reverse()
171 | ->__to_array()
172 | ;
173 |
174 | $arr = $enum->__map(function($value, $index){
175 | // do stuff
176 |
177 | return $stuff;
178 | });
179 |
180 | $enum;
181 |
182 | ```
183 |
--------------------------------------------------------------------------------
/src/ArrayHelpers/ByteArrayDispenser.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\Traits\ArrayHelpers;
14 |
15 | use chillerlan\Traits\TraitException;
16 | use Exception, Traversable;
17 |
18 | /**
19 | *
20 | */
21 | class ByteArrayDispenser{
22 |
23 | /**
24 | * @var string
25 | */
26 | protected $byteArrayClass = ByteArray::class;
27 |
28 | /**
29 | * @param int $int
30 | *
31 | * @return bool
32 | */
33 | public function isAllowedInt(int $int):bool{
34 | return $int >= 0 && $int <= PHP_INT_MAX;
35 | }
36 |
37 | /**
38 | * @param int $size
39 | *
40 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray
41 | * @throws \chillerlan\Traits\TraitException
42 | */
43 | public function fromIntSize(int $size):ByteArray{
44 |
45 | if(!$this->isAllowedInt($size)){
46 | throw new TraitException('invalid size');
47 | }
48 |
49 | return new $this->byteArrayClass($size);
50 | }
51 |
52 | /**
53 | * @param array $array
54 | * @param bool $save_indexes
55 | *
56 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray
57 | * @throws \chillerlan\Traits\TraitException
58 | */
59 | public function fromArray(array $array, bool $save_indexes = null):ByteArray{
60 |
61 | try{
62 | $out = $this->fromIntSize(\count($array));
63 |
64 | $array = ($save_indexes ?? true) ? $array : \array_values($array);
65 |
66 | foreach($array as $k => $v){
67 | $out[$k] = $v;
68 | }
69 |
70 | return $out;
71 | }
72 | // this can be anything
73 | // @codeCoverageIgnoreStart
74 | catch(Exception $e){
75 | throw new TraitException($e->getMessage());
76 | }
77 | // @codeCoverageIgnoreEnd
78 |
79 | }
80 |
81 | /**
82 | * @param int $len
83 | *
84 | * @param mixed $fill
85 | *
86 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray
87 | * @throws \chillerlan\Traits\TraitException
88 | */
89 | public function fromArrayFill(int $len, $fill = null):ByteArray{
90 |
91 | if(!$this->isAllowedInt($len)){
92 | throw new TraitException('invalid length');
93 | }
94 |
95 | return $this->fromArray(\array_fill(0, $len, $fill));
96 | }
97 |
98 | /**
99 | * @param string $str
100 | *
101 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray
102 | */
103 | public function fromString(string $str):ByteArray{
104 | return $this->fromArray(\unpack('C*', $str), false);
105 | }
106 |
107 | /**
108 | * checks if the given string is a hex string: ab12cd34 (case insensitive, whitespace allowed)
109 | *
110 | * @param string $hex
111 | *
112 | * @return bool
113 | */
114 | public function isAllowedHex(string $hex):bool{
115 | return \preg_match('/^[\s\r\n\t \da-f]+$/i', $hex) && \strlen($hex) % 2 === 0;
116 | }
117 |
118 | /**
119 | * @param string $hex
120 | *
121 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray|mixed
122 | * @throws \chillerlan\Traits\TraitException
123 | */
124 | public function fromHex(string $hex):ByteArray{
125 | $hex = \preg_replace('/[\s\r\n\t ]/', '', $hex);
126 |
127 | if(!$this->isAllowedHex($hex)){
128 | throw new TraitException('invalid hex string');
129 | }
130 |
131 | return $this->fromString(\pack('H*', $hex));
132 | }
133 |
134 | /**
135 | * checks if the given (trimmed) JSON string is a an array that contains numbers: [1, 2, 3]
136 | *
137 | * @param string $json
138 | *
139 | * @return bool
140 | */
141 | public function isAllowedJSON(string $json):bool{
142 | return \preg_match('/^\\[[\s\d,]+\\]$/', $json) > 0;
143 | }
144 |
145 | /**
146 | * @param string $json
147 | *
148 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray|mixed
149 | * @throws \chillerlan\Traits\TraitException
150 | */
151 | public function fromJSON(string $json):ByteArray{
152 | $json = \trim($json);
153 |
154 | if(!$this->isAllowedJSON($json)){
155 | throw new TraitException('invalid JSON array');
156 | }
157 |
158 | return $this->fromArray(\json_decode(\trim($json)));
159 | }
160 |
161 | /**
162 | * checks if the given (trimmed) string is base64 encoded binary
163 | *
164 | * @param string $base64
165 | *
166 | * @return bool
167 | */
168 | public function isAllowedBase64(string $base64):bool{
169 | return \preg_match('#^[a-z\d/]*={0,2}$#i', $base64) > 0;
170 | }
171 |
172 | /**
173 | * @param string $base64
174 | *
175 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray|mixed
176 | * @throws \chillerlan\Traits\TraitException
177 | */
178 | public function fromBase64(string $base64):ByteArray{
179 | $base64 = \trim($base64);
180 |
181 | if(!$this->isAllowedBase64($base64)){
182 | throw new TraitException('invalid base64 string');
183 | }
184 |
185 | return $this->fromString(\base64_decode($base64));
186 | }
187 |
188 | /**
189 | * checks if the given (trimmed) string is a binary string: [01] in multiples of 8
190 | *
191 | * @param string $bin
192 | *
193 | * @return bool
194 | */
195 | public function isAllowedBin(string $bin):bool{
196 | return \preg_match('/^[01]+$/', $bin) > 0 && \strlen($bin) % 8 === 0;
197 | }
198 |
199 | /**
200 | * @param string $bin
201 | *
202 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray
203 | * @throws \chillerlan\Traits\TraitException
204 | */
205 | public function fromBin(string $bin):ByteArray{
206 | $bin = \trim($bin);
207 |
208 | if(!$this->isAllowedBin($bin)){
209 | throw new TraitException('invalid binary string');
210 | }
211 |
212 | return $this->fromArray(\array_map('bindec', \str_split($bin, 8)));
213 | }
214 |
215 | /**
216 | * @param string|array|\SplFixedArray $data
217 | *
218 | * @return \chillerlan\Traits\ArrayHelpers\ByteArray
219 | * @throws \chillerlan\Traits\TraitException
220 | */
221 | public function guessFrom($data):ByteArray{
222 |
223 | if($data instanceof Traversable){
224 | return $this->fromArray(\iterator_to_array($data));
225 | }
226 |
227 | if(\is_array($data)){
228 | return $this->fromArray($data);
229 | }
230 |
231 | if(\is_string($data)){
232 |
233 | foreach(['Bin', 'Hex', 'JSON', 'Base64'] as $type){
234 |
235 | if(\call_user_func_array([$this, 'isAllowed'.$type], [$data]) === true){
236 | return \call_user_func_array([$this, 'from'.$type], [$data]);
237 | }
238 |
239 | }
240 |
241 | return $this->fromString($data);
242 | }
243 |
244 | throw new TraitException('invalid input');
245 | }
246 |
247 | }
248 |
--------------------------------------------------------------------------------
/tests/ArrayTrait/gw2api-floors-2-1.json:
--------------------------------------------------------------------------------
1 | {
2 | "texture_dims": [
3 | 16384,
4 | 16384
5 | ],
6 | "clamped_view": [
7 | [
8 | 0,
9 | 4094
10 | ],
11 | [
12 | 16382,
13 | 16382
14 | ]
15 | ],
16 | "regions": {
17 | "7": {
18 | "name": "World vs. World",
19 | "label_coord": [
20 | 19840,
21 | 13568
22 | ],
23 | "continent_rect": [
24 | [
25 | 17664,
26 | 8192
27 | ],
28 | [
29 | 23808,
30 | 21376
31 | ]
32 | ],
33 | "maps": {
34 | "38": {
35 | "name": "Eternal Battlegrounds",
36 | "min_level": 80,
37 | "max_level": 80,
38 | "default_floor": 3,
39 | "label_coord": [
40 | 10700,
41 | 14800
42 | ],
43 | "map_rect": [
44 | [
45 | -36864,
46 | -36864
47 | ],
48 | [
49 | 36864,
50 | 36864
51 | ]
52 | ],
53 | "continent_rect": [
54 | [
55 | 8958,
56 | 12798
57 | ],
58 | [
59 | 12030,
60 | 15870
61 | ]
62 | ],
63 | "points_of_interest": {
64 | "990": {
65 | "name": "Stonemist Keep",
66 | "type": "landmark",
67 | "floor": 3,
68 | "coord": [
69 | 10606.1,
70 | 14549.6
71 | ],
72 | "id": 990,
73 | "chat_link": "[&BN4DAAA=]"
74 | },
75 | "2293": {
76 | "name": "Valley Emergency Waypoint",
77 | "type": "waypoint",
78 | "floor": 3,
79 | "coord": [
80 | 11587.2,
81 | 15145.7
82 | ],
83 | "id": 2293,
84 | "chat_link": "[&BPUIAAA=]"
85 | }
86 | },
87 | "tasks": {},
88 | "skill_challenges": [
89 | {
90 | "coord": [
91 | 11127.8,
92 | 14246.9
93 | ],
94 | "id": "0-225"
95 | },
96 | {
97 | "coord": [
98 | 10007.2,
99 | 14220.6
100 | ],
101 | "id": "0-224"
102 | },
103 | {
104 | "coord": [
105 | 10604.1,
106 | 14525.8
107 | ],
108 | "id": "0-226"
109 | },
110 | {
111 | "coord": [
112 | 10657.2,
113 | 15627.2
114 | ],
115 | "id": "0-223"
116 | }
117 | ],
118 | "sectors": {
119 | "833": {
120 | "name": "Stonemist Castle",
121 | "level": 0,
122 | "coord": [
123 | 10643.9,
124 | 14563.8
125 | ],
126 | "bounds": [
127 | [
128 | 10233.8,
129 | 14851.7
130 | ],
131 | [
132 | 10120.4,
133 | 14676.4
134 | ],
135 | [
136 | 10181.3,
137 | 14473.4
138 | ],
139 | [
140 | 10222.5,
141 | 14191.7
142 | ],
143 | [
144 | 10475.5,
145 | 14146.4
146 | ],
147 | [
148 | 10803.8,
149 | 14153.2
150 | ],
151 | [
152 | 11012.4,
153 | 14218.5
154 | ],
155 | [
156 | 11057.8,
157 | 14343.8
158 | ],
159 | [
160 | 11010.2,
161 | 14500.5
162 | ],
163 | [
164 | 11055.5,
165 | 14788.7
166 | ],
167 | [
168 | 10950.6,
169 | 14944.6
170 | ],
171 | [
172 | 10768.4,
173 | 15051.6
174 | ],
175 | [
176 | 10479.1,
177 | 14988.8
178 | ],
179 | [
180 | 10233.8,
181 | 14851.7
182 | ]
183 | ],
184 | "id": 833,
185 | "chat_link": "[&BEEDAAA=]"
186 | },
187 | "1031": {
188 | "name": "Zraith's Beacon",
189 | "level": 0,
190 | "coord": [
191 | 11511.2,
192 | 13437.4
193 | ],
194 | "bounds": [
195 | [
196 | 11534,
197 | 14108.1
198 | ],
199 | [
200 | 11391.5,
201 | 13918.9
202 | ],
203 | [
204 | 11353.6,
205 | 13694.5
206 | ],
207 | [
208 | 11228.3,
209 | 13489.9
210 | ],
211 | [
212 | 10930.5,
213 | 13126
214 | ],
215 | [
216 | 11221.5,
217 | 12956.2
218 | ],
219 | [
220 | 11333.5,
221 | 12877.2
222 | ],
223 | [
224 | 11443.8,
225 | 12849.3
226 | ],
227 | [
228 | 11631,
229 | 12948.8
230 | ],
231 | [
232 | 11718,
233 | 13290.5
234 | ],
235 | [
236 | 11991.1,
237 | 13477.3
238 | ],
239 | [
240 | 12079.8,
241 | 13842.5
242 | ],
243 | [
244 | 11788.6,
245 | 14107.5
246 | ],
247 | [
248 | 11534,
249 | 14108.1
250 | ]
251 | ],
252 | "id": 1031,
253 | "chat_link": "[&BAcEAAA=]"
254 | }
255 | },
256 | "adventures": [],
257 | "id": 38,
258 | "mastery_points": []
259 | }
260 | },
261 | "id": 7
262 | }
263 | },
264 | "id": 1
265 | }
--------------------------------------------------------------------------------
/tests/ArrayTrait/ByteArrayTest.php:
--------------------------------------------------------------------------------
1 |
9 | * @copyright 2017 Smiley
10 | * @license MIT
11 | */
12 |
13 | namespace chillerlan\TraitTest;
14 |
15 | use chillerlan\Traits\ArrayHelpers\ByteArrayDispenser;
16 | use chillerlan\Traits\TraitException;
17 | use PHPUnit\Framework\TestCase;
18 |
19 | class ByteArrayTest extends TestCase{
20 |
21 | /**
22 | * @var \chillerlan\Traits\ArrayHelpers\ByteArrayDispenser
23 | */
24 | protected $arrayDispenser;
25 |
26 | protected function setUp():void{
27 | $this->arrayDispenser = new ByteArrayDispenser;
28 | }
29 |
30 | public function testConvert(){
31 | $hex_input = '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f';
32 |
33 | $expected_bin = '0000000000000001000000100000001100000100000001010000011000000111000010000000100100001010000010110000110000001101000011100000111100010000000100010001001000010011000101000001010100010110000101110001100000011001000110100001101100011100000111010001111000011111';
34 | $expected_b64 = 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=';
35 | $expected_json = '[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]';
36 | $expected_arr = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31];
37 | $expected_str = hex2bin($hex_input);
38 |
39 | $this->assertSame($expected_bin, $this->arrayDispenser->fromHex($hex_input)->toBin());
40 | $this->assertSame($expected_b64, $this->arrayDispenser->fromHex($hex_input)->toBase64());
41 | $this->assertSame($expected_json, $this->arrayDispenser->fromHex($hex_input)->toJSON());
42 | $this->assertSame($expected_arr, $this->arrayDispenser->fromHex($hex_input)->toArray());
43 | $this->assertSame($expected_str, $this->arrayDispenser->fromHex($hex_input)->toString());
44 |
45 | $this->assertSame($hex_input, $this->arrayDispenser->fromBin($expected_bin)->toHex());
46 | $this->assertSame($hex_input, $this->arrayDispenser->fromBase64($expected_b64)->toHex());
47 | $this->assertSame($hex_input, $this->arrayDispenser->fromJSON($expected_json)->toHex());
48 | $this->assertSame($hex_input, $this->arrayDispenser->fromArray($expected_arr)->toHex());
49 | $this->assertSame($hex_input, $this->arrayDispenser->fromString($expected_str)->toHex());
50 | }
51 |
52 | public function testFromfromIntSize(){
53 | $this->assertSame([null,null,null,null], $this->arrayDispenser->fromIntSize(4)->toArray());
54 | }
55 |
56 | public function testFromArrayFill(){
57 | $this->assertSame([42,42,42], $this->arrayDispenser->fromArrayFill(3, 42)->toArray());
58 | $this->assertSame([[],[],[]], $this->arrayDispenser->fromArrayFill(3, [])->toArray());
59 | }
60 |
61 | public function guessFromData(){
62 | return [
63 | 'hex' => ['000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'],
64 | 'bin' => ['0000000000000001000000100000001100000100000001010000011000000111000010000000100100001010000010110000110000001101000011100000111100010000000100010001001000010011000101000001010100010110000101110001100000011001000110100001101100011100000111010001111000011111'],
65 | 'base64' => ['AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8='],
66 | 'json' => ['[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]'],
67 | 'array' => [[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31]],
68 | 'string' => [hex2bin('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f')],
69 | 'ByteArray' => [(new ByteArrayDispenser())->fromHex('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f')],
70 | ];
71 | }
72 |
73 | /**
74 | * @dataProvider guessFromData
75 | */
76 | public function testGuessFrom($from){
77 | $this->assertSame('000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', $this->arrayDispenser->guessFrom($from)->toHex());
78 | }
79 |
80 | public function testGuessFromInvalidData(){
81 | $this->expectException(TraitException::class);
82 | $this->expectExceptionMessage('invalid input');
83 |
84 | $this->arrayDispenser->guessFrom(new \stdClass);
85 | }
86 |
87 | public function testInvalidIntException(){
88 | $this->expectException(TraitException::class);
89 | $this->expectExceptionMessage('invalid size');
90 |
91 | $this->arrayDispenser->fromIntSize(-1);
92 | }
93 |
94 | public function testInvalidRangeException(){
95 | $this->expectException(TraitException::class);
96 | $this->expectExceptionMessage('invalid length');
97 |
98 | $this->arrayDispenser->fromArrayFill(-1, 1);
99 | }
100 |
101 | public function testInvalidHexException(){
102 | $this->expectException(TraitException::class);
103 | $this->expectExceptionMessage('invalid hex string');
104 |
105 | $this->arrayDispenser->fromHex('foo');
106 | }
107 |
108 | public function testInvalidJSONException(){
109 | $this->expectException(TraitException::class);
110 | $this->expectExceptionMessage('invalid JSON array');
111 |
112 | $this->arrayDispenser->fromJSON('{}');
113 | }
114 |
115 | public function testInvalidBase64Exception(){
116 | $this->expectException(TraitException::class);
117 | $this->expectExceptionMessage('invalid base64 string');
118 |
119 | $this->arrayDispenser->fromBase64('\\');
120 | }
121 |
122 | public function testInvalidBinException(){
123 | $this->expectException(TraitException::class);
124 | $this->expectExceptionMessage('invalid binary string');
125 |
126 | $this->arrayDispenser->fromBin('2');
127 | }
128 |
129 | public function testCopyFrom(){
130 | $h1 = '000102030405060708090a0b0c0d0e0f';
131 | $h2 = '101112131415161718191a1b1c1d1e1f';
132 |
133 | $b2 = $this->arrayDispenser->fromHex($h2);
134 |
135 | // note the "referenced" behaviour of \SplFixedArray
136 | $this->assertSame($h1.$h2, $this->arrayDispenser->fromHex($h1)->copyFrom($b2)->toHex());
137 | $this->assertSame($h2, $this->arrayDispenser->fromHex($h1)->copyFrom($b2, null, 0)->toHex());
138 | $this->assertSame($h2, $this->arrayDispenser->fromHex($h1)->copyFrom($b2, 16, 0)->toHex());
139 | $this->assertSame('101112131415161708090a0b0c0d0e0f', $this->arrayDispenser->fromHex($h1)->copyFrom($b2, 8, 0)->toHex());
140 | }
141 |
142 | public function testSlice(){
143 | $h1 = '000102030405060708090a0b0c0d0e0f';
144 | $h2 = '101112131415161718191a1b1c1d1e1f';
145 |
146 | $b = $this->arrayDispenser->fromHex($h1.$h2);
147 |
148 | $this->assertSame($h1, $b->slice(0, 16)->toHex());
149 | $this->assertSame($h2, $b->slice(16, 16)->toHex());
150 | }
151 |
152 | public function testEqual(){
153 | $h1 = '000102030405060708090a0b0c0d0e0f';
154 |
155 | $b = $this->arrayDispenser->fromHex($h1);
156 |
157 | $this->assertTrue($b->equal($this->arrayDispenser->fromHex($h1)));
158 | $this->assertTrue($b->equal($this->arrayDispenser->fromArray([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15])));
159 |
160 | $this->assertFalse($b->equal($this->arrayDispenser->fromHex('000102030405060708090a0b0c0d0e0e')));
161 | $this->assertFalse($b->equal($this->arrayDispenser->fromHex('000102030405060708090a0b0c0d0e')));
162 | }
163 |
164 | }
165 |
--------------------------------------------------------------------------------