├── .github
├── FUNDING.yml
├── SECURITY.md
└── workflows
│ ├── bc-check.yml
│ ├── phpstan.yml
│ ├── infection.yml
│ └── run-tests.yml
├── .gitignore
├── infection.json5
├── phpstan.neon
├── tests
├── NullifyExtendTest.php
├── NullifyArrayAccessTest.php
├── NullifyTest.php
└── NullifyCollectionTest.php
├── phpunit.xml
├── LICENSE.md
├── composer.json
├── src
└── Nullify.php
└── README.md
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | custom: ["https://paypal.com/donate/?hosted_button_id=KHLEL8PFS4AXJ"]
2 |
--------------------------------------------------------------------------------
/.github/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | If you discover any security related issues, please email michael@laravel.software instead of using the issue tracker.
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .phpunit.result.cache
3 | .phpunit.cache
4 | coverage
5 | docs
6 | testbench.yaml
7 | vendor
8 | node_modules
9 | infection.log
10 | build
11 |
--------------------------------------------------------------------------------
/infection.json5:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "vendor/infection/infection/resources/schema.json",
3 | "source": {
4 | "directories": [
5 | "src"
6 | ]
7 | },
8 | // "logs": {
9 | // "text": "php://stderr",
10 | // "github": true
11 | // },
12 | "logs": {
13 | "text": "infection.log"
14 | },
15 | "mutators": {
16 | "@default": true,
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/phpstan.neon:
--------------------------------------------------------------------------------
1 | parameters:
2 |
3 | paths:
4 | - src
5 |
6 | level: max
7 |
8 | reportUnmatchedIgnoredErrors: false
9 |
10 | ignoreErrors:
11 | - '#Argument of an invalid type ArrayAccess\|iterable supplied for foreach, only iterables are supported\.#'
12 | - '#Cannot access offset mixed on array\|ArrayAccess\|\(iterable&object\)\.#'
13 | - '#Cannot access offset mixed on mixed\.#'
14 |
--------------------------------------------------------------------------------
/tests/NullifyExtendTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
15 | }
16 | }
17 |
18 | class NewNullify extends Nullify
19 | {
20 | public function __construct()
21 | {
22 | static::nullify('');
23 | static::blank('');
24 | static::clone('');
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/bc-check.yml:
--------------------------------------------------------------------------------
1 | name: bc-check
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 | branches:
9 | - main
10 |
11 | jobs:
12 | backwards-compatibility-check:
13 | name: Backwards Compatibility Check
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v2
17 | with:
18 | fetch-depth: 0
19 | - name: "Install PHP"
20 | uses: shivammathur/setup-php@v2
21 | with:
22 | php-version: "8.3"
23 | - name: "Install dependencies"
24 | run: "composer install"
25 | - name: "Install BC check"
26 | run: "composer require --dev roave/backward-compatibility-check"
27 | - name: "Check for BC breaks"
28 | run: "vendor/bin/roave-backward-compatibility-check"
29 |
--------------------------------------------------------------------------------
/.github/workflows/phpstan.yml:
--------------------------------------------------------------------------------
1 | name: phpstan
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | phpstan:
11 | name: "Running Larastan check"
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Setup PHP
17 | uses: shivammathur/setup-php@v2
18 | with:
19 | php-version: '8.3'
20 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl
21 | coverage: none
22 |
23 | - name: Cache composer dependencies
24 | uses: actions/cache@v2
25 | with:
26 | path: vendor
27 | key: composer-${{ hashFiles('composer.lock') }}
28 |
29 | - name: Run composer install
30 | run: composer install -n --prefer-dist
31 |
32 | - name: Run phpstan
33 | run: ./vendor/bin/phpstan
34 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | tests
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ./src
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/.github/workflows/infection.yml:
--------------------------------------------------------------------------------
1 | name: infection
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | infection:
11 | name: "Running Infection"
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Setup PHP
17 | uses: shivammathur/setup-php@v2
18 | with:
19 | php-version: '8.3'
20 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, fileinfo, sodium
21 | coverage: xdebug
22 |
23 | - name: Cache composer dependencies
24 | uses: actions/cache@v2
25 | with:
26 | path: vendor
27 | key: composer-${{ hashFiles('composer.lock') }}
28 |
29 | - name: Run composer install
30 | run: composer install -n --prefer-dist
31 |
32 | - name: Run infection
33 | run: ./vendor/bin/infection --show-mutations --min-msi=100 --min-covered-msi=100
34 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Michael Rubél
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 all
13 | 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 THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/run-tests.yml:
--------------------------------------------------------------------------------
1 | name: run-tests
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | pull_request:
7 | branches: [main]
8 |
9 | jobs:
10 | test:
11 | runs-on: ${{ matrix.os }}
12 | strategy:
13 | fail-fast: true
14 | matrix:
15 | os: [ubuntu-latest, windows-latest]
16 | php: [8.0, 8.1, 8.2, 8.3, 8.4]
17 | stability: [prefer-lowest, prefer-stable]
18 |
19 | name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }}
20 |
21 | steps:
22 | - name: Checkout code
23 | uses: actions/checkout@v2
24 |
25 | - name: Setup PHP
26 | uses: shivammathur/setup-php@v2
27 | with:
28 | php-version: ${{ matrix.php }}
29 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, intl, fileinfo, sodium
30 | coverage: pcov
31 |
32 | - name: Setup problem matchers
33 | run: |
34 | echo "::add-matcher::${{ runner.tool_cache }}/php.json"
35 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
36 |
37 | - name: Install dependencies
38 | run: composer update --prefer-dist --no-interaction
39 |
40 | - name: Execute tests
41 | run: vendor/bin/phpunit --coverage-clover build/clover.xml
42 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "michael-rubel/nullify",
3 | "description": "Convert empty data of any type to null.",
4 | "keywords": [
5 | "michael-rubel",
6 | "nullify"
7 | ],
8 | "homepage": "https://github.com/michael-rubel/nullify",
9 | "license": "MIT",
10 | "authors": [
11 | {
12 | "name": "Michael Rubel",
13 | "email": "michael@laravel.software",
14 | "homepage": "https://laravel.software",
15 | "role": "Developer"
16 | }
17 | ],
18 | "require": {
19 | "php": "^8.0"
20 | },
21 | "require-dev": {
22 | "illuminate/support": "*",
23 | "infection/infection": "^0.26",
24 | "phpstan/phpstan": "^1.10",
25 | "phpunit/phpunit": "^9.5|^10.5",
26 | "symfony/var-dumper": "^5.4|^6.1"
27 | },
28 | "autoload": {
29 | "psr-4": {
30 | "MichaelRubel\\Nullify\\": "src"
31 | }
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "MichaelRubel\\Nullify\\Tests\\": "tests"
36 | }
37 | },
38 | "scripts": {
39 | "test": "./vendor/bin/phpunit --no-coverage",
40 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage"
41 | },
42 | "config": {
43 | "sort-packages": true,
44 | "allow-plugins": {
45 | "infection/extension-installer": true
46 | }
47 | },
48 | "minimum-stability": "dev",
49 | "prefer-stable": true
50 | }
51 |
--------------------------------------------------------------------------------
/src/Nullify.php:
--------------------------------------------------------------------------------
1 |
19 | */
20 | class Nullify
21 | {
22 | /**
23 | * Perform the "Nullification". 😁
24 | *
25 | * @param mixed $values
26 | *
27 | * @return mixed
28 | */
29 | public static function the(mixed $values): mixed
30 | {
31 | return static::nullify($values);
32 | }
33 |
34 | /**
35 | * "Nullify" the value or iterable.
36 | *
37 | * @param mixed $value
38 | *
39 | * @return mixed
40 | */
41 | protected static function nullify(mixed $value): mixed
42 | {
43 | if (is_null($value) || static::blank($value)) {
44 | return null;
45 | }
46 |
47 | if (! is_iterable($value) && ! $value instanceof \ArrayAccess) {
48 | return $value;
49 | }
50 |
51 | $output = static::clone($value);
52 |
53 | foreach ($value as $key => $nested) {
54 | $output[$key] = static::nullify($nested);
55 | }
56 |
57 | return $output;
58 | }
59 |
60 | /**
61 | * Determine if the given value is "blank".
62 | *
63 | * @param mixed $value
64 | *
65 | * @return bool
66 | */
67 | protected static function blank(mixed $value): bool
68 | {
69 | if (is_string($value)) {
70 | return trim($value) === '';
71 | }
72 |
73 | if (is_numeric($value) || is_bool($value)) {
74 | return false;
75 | }
76 |
77 | if ($value instanceof \Countable) {
78 | return count($value) === 0;
79 | }
80 |
81 | if (is_object($value)) {
82 | return empty((array) $value);
83 | }
84 |
85 | return empty($value);
86 | }
87 |
88 | /**
89 | * Clone the object or return the given value.
90 | *
91 | * @param mixed $value
92 | *
93 | * @return mixed
94 | */
95 | protected static function clone(mixed $value): mixed
96 | {
97 | return is_object($value) ? clone $value : $value;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/NullifyArrayAccessTest.php:
--------------------------------------------------------------------------------
1 | assertSame($result['test'], Nullify::the($value)['test']);
20 | }
21 |
22 | public function testCanNullifyValuesInNestedArrayAccess()
23 | {
24 | $value = new ArrayAccessObject;
25 | $nested = new ArrayAccessObject;
26 | $nested['test'] = '';
27 | $value['obj'] = $nested;
28 | $this->assertSame('', $value['obj']['test']);
29 |
30 | $result = new ArrayAccessObject;
31 | $nested = new ArrayAccessObject;
32 | $nested['test'] = null;
33 | $result['obj'] = $nested;
34 | $this->assertSame(null, $result['obj']['test']);
35 |
36 | $this->assertEquals($result, Nullify::the($value));
37 | }
38 |
39 | public function testObjectIsImmutableAfterUsingNullify()
40 | {
41 | $value = new ArrayAccessObject;
42 | $nested = new ArrayAccessObject;
43 | $nested['test'] = '';
44 | $value['obj'] = $nested;
45 | $this->assertSame('', $value['obj']['test']);
46 |
47 | $nullified = Nullify::the($value);
48 | $this->assertEquals($nested, $nullified['obj']);
49 | $this->assertSame(null, $nullified['obj']['test']);
50 | $this->assertSame('', $value['obj']['test']);
51 | }
52 |
53 | public function testArrayIsImmutableAfterUsingNullify()
54 | {
55 | $value = [];
56 | $nested = [];
57 | $nested['test'] = '';
58 | $value['obj'] = $nested;
59 | $this->assertSame('', $value['obj']['test']);
60 |
61 | $nullified = Nullify::the($value);
62 | $this->assertEquals($nested, $nullified['obj']);
63 | $this->assertSame(null, $nullified['obj']['test']);
64 | $this->assertSame('', $value['obj']['test']);
65 | }
66 | }
67 |
68 | class ArrayAccessObject implements ArrayAccess
69 | {
70 | public object $obj;
71 | public ?string $test;
72 |
73 | public function offsetExists(mixed $offset): bool
74 | {
75 | return isset($this->{$offset});
76 | }
77 |
78 | public function offsetGet(mixed $offset): mixed
79 | {
80 | return $this->{$offset};
81 | }
82 |
83 | public function offsetSet(mixed $offset, mixed $value): void
84 | {
85 | $this->{$offset} = $value;
86 | }
87 |
88 | public function offsetUnset(mixed $offset): void
89 | {
90 | unset($this->{$offset});
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/tests/NullifyTest.php:
--------------------------------------------------------------------------------
1 | assertNull(Nullify::the($value));
14 | }
15 |
16 | public function testCanPassInt()
17 | {
18 | $value = 0;
19 | $this->assertSame(0, Nullify::the($value));
20 | }
21 |
22 | public function testCanPassString()
23 | {
24 | $value = '';
25 | $this->assertNull(Nullify::the($value));
26 |
27 | $value = 'test';
28 | $this->assertSame($value, Nullify::the($value));
29 | }
30 |
31 | public function testCanPassEmptyArray()
32 | {
33 | $value = [];
34 | $this->assertNull(Nullify::the($value));
35 |
36 | $value = ['test'];
37 | $this->assertSame($value, Nullify::the($value));
38 | }
39 |
40 | public function testCanPassObject()
41 | {
42 | $value = (object) ['test'];
43 | $this->assertSame($value, Nullify::the($value));
44 | }
45 |
46 | public function testCanPassEmptyObject()
47 | {
48 | $value = (object) [];
49 | $this->assertNull(Nullify::the($value));
50 |
51 | $value = new \stdClass;
52 | $this->assertNull(Nullify::the($value));
53 | }
54 |
55 | public function testNullifiesNestedArrays()
56 | {
57 | $value = [
58 | 'one' => [
59 | 'one_and_half' => [],
60 | 'two' => ['three' => ['four' => '']]
61 | ]
62 | ];
63 |
64 | $this->assertSame([
65 | 'one' => [
66 | 'one_and_half' => null,
67 | 'two' => ['three' => ['four' => null]]
68 | ]
69 | ], Nullify::the($value));
70 | }
71 |
72 | public function testLeavesNullAsNullInArray()
73 | {
74 | $value = [
75 | 'test' => null,
76 | ];
77 |
78 | $this->assertSame($value, Nullify::the($value));
79 | }
80 |
81 | public function testLeavesIntAsIntInArray()
82 | {
83 | $value = [
84 | 'test' => 0,
85 | ];
86 |
87 | $this->assertSame($value, Nullify::the($value));
88 | }
89 |
90 | public function testLeavesStringAsStringInArray()
91 | {
92 | $value = [
93 | 'test' => 'test',
94 | ];
95 |
96 | $this->assertSame($value, Nullify::the($value));
97 | }
98 |
99 | public function testLeavesObjectAsObjectInArray()
100 | {
101 | $value = [
102 | 'test' => (object) ['test'],
103 | ];
104 |
105 | $this->assertSame($value, Nullify::the($value));
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | # Nullify
4 | [](https://github.com/michael-rubel/nullify/actions/workflows/run-tests.yml)
5 | [](https://github.com/michael-rubel/nullify/actions/workflows/infection.yml)
6 | [](https://github.com/michael-rubel/nullify/actions/workflows/bc-check.yml)
7 | [](https://github.com/michael-rubel/nullify/actions/workflows/phpstan.yml)
8 |
9 | A plain PHP class to convert empty data of any type to `null`
10 |
11 | PHP `^8.0` is required to use this class.
12 |
13 | ## Installation
14 |
15 | ```bash
16 | composer require michael-rubel/nullify
17 | ```
18 |
19 | ## Usage
20 |
21 | ```php
22 | use MichaelRubel\Nullify\Nullify;
23 |
24 | Nullify::the($value);
25 | ```
26 |
27 | - **Note:** the class checks also nested [iterables](https://www.php.net/manual/en/function.is-iterable.php) and [ArrayAccess](https://www.php.net/manual/en/class.arrayaccess.php) objects.
28 |
29 | ## Examples
30 | ```php
31 | $value = null;
32 | Nullify::the($value); // null
33 |
34 | $value = '';
35 | Nullify::the($value); // null
36 |
37 | $value = [];
38 | Nullify::the($value); // null
39 |
40 | $value = (object) [];
41 | Nullify::the($value); // null
42 |
43 | $value = new \stdClass;
44 | Nullify::the($value); // null
45 | ```
46 |
47 | ---
48 |
49 | ⚡ Check nested elements:
50 |
51 | ```php
52 | $values = new Collection([
53 | 'valid' => true,
54 | 'empty_array' => [],
55 | 'empty_string' => '',
56 | 'collection' => new Collection([
57 | 'invalid' => new \stdClass,
58 | ])
59 | ]);
60 |
61 | Nullify::the($values);
62 |
63 | // Illuminate\Support\Collection^ {#459
64 | // #items: array:4 [
65 | // "valid" => true
66 | // "empty_array" => null
67 | // "empty_string" => null
68 | // "collection" => Illuminate\Support\Collection^ {#461
69 | // #items: array:1 [
70 | // "invalid" => null
71 | // ]
72 | // #escapeWhenCastingToString: false
73 | // }
74 | // ]
75 | // #escapeWhenCastingToString: false
76 | // }
77 | ```
78 |
79 | ---
80 |
81 | 📚 If you use [Laravel Collections](https://laravel.com/docs/master/collections), you can make a macro:
82 |
83 | ```php
84 | Collection::macro('nullify', function () {
85 | return $this->map(fn ($value) => Nullify::the($value));
86 | });
87 |
88 | collect(['', [], (object) [], new \stdClass, '✔'])
89 | ->nullify()
90 | ->toArray(); // [0 => null, 1 => null, 2 => null, 3 => null, 4 => '✔']
91 | ```
92 |
93 | ## Testing
94 | ```bash
95 | composer test
96 | ```
97 |
98 | ## License
99 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
100 |
--------------------------------------------------------------------------------
/tests/NullifyCollectionTest.php:
--------------------------------------------------------------------------------
1 | assertNull((new Collection)->nullify());
25 | }
26 |
27 | public function testCanNullifyValuesInCollection(): void
28 | {
29 | $result = Collection::make([
30 | 'null' => null,
31 | 'empty' => '',
32 | 'space' => ' ',
33 | 'space_with_content' => ' test ',
34 | 'array' => [],
35 | 'collection' => collect(),
36 | 'countable' => new \SplObjectStorage,
37 | 'correct_one' => 'Correct one!',
38 | ])->nullify()->toArray();
39 |
40 | $this->assertSame([
41 | 'null' => null,
42 | 'empty' => null,
43 | 'space' => null,
44 | 'space_with_content' => ' test ',
45 | 'array' => null,
46 | 'collection' => null,
47 | 'countable' => null,
48 | 'correct_one' => 'Correct one!',
49 | ], $result);
50 | }
51 |
52 | public function testCanNullifyValuesWithNestedArrayAccess(): void
53 | {
54 | $result = Collection::make([
55 | 'test' => new \SplObjectStorage,
56 | 'first_name' => collect(['first_part' => false, 'last_part' => '']),
57 | 'last_name' => collect(['first_part' => true, 'last_part' => []]),
58 | 'full_name' => collect(['first_part' => new \SplObjectStorage, 'last_part' => collect(['additional_part' => ['next_part' => ['deep_part' => '']]])]),
59 | ])->nullify()->toArray();
60 |
61 | $this->assertSame([
62 | 'test' => null,
63 | 'first_name' => ['first_part' => false, 'last_part' => null],
64 | 'last_name' => ['first_part' => true, 'last_part' => null],
65 | 'full_name' => ['first_part' => null, 'last_part' => ['additional_part' => ['next_part' => ['deep_part' => null]]]],
66 | ], $result);
67 | }
68 |
69 | /** @test */
70 | public function testCanNullifyValuesWithoutConvertingToArray(): void
71 | {
72 | $nullified = Collection::make([
73 | 'first_name' => ['first_part' => false, 'last_part' => ''],
74 | 'last_name' => collect(['first_part' => false, 'last_part' => []]),
75 | 'full_name' => ['first_part' => true, 'last_part' => collect(['additional_part' => ''])],
76 | ])->nullify();
77 |
78 | $expected = Collection::make([
79 | 'first_name' => ['first_part' => false, 'last_part' => null],
80 | 'last_name' => collect(['first_part' => false, 'last_part' => null]),
81 | 'full_name' => ['first_part' => true, 'last_part' => collect(['additional_part' => null])],
82 | ]);
83 |
84 | $this->assertEquals($expected, $nullified);
85 | }
86 |
87 | /** @test */
88 | public function testArrayIteratorBehavesAsExpectedWhenNullify(): void
89 | {
90 | $nullified = Collection::make(['iterator' => new \ArrayIterator([1, 2, 3])])->nullify();
91 | $expected = Collection::make(['iterator' => new \ArrayIterator([1, 2, 3])]);
92 | $this->assertEquals($expected, $nullified);
93 |
94 | $nullified = Collection::make(['iterator' => new \ArrayIterator])->nullify();
95 | $expected = Collection::make(['iterator' => null]);
96 | $this->assertEquals($expected, $nullified);
97 | }
98 | }
--------------------------------------------------------------------------------