Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support union types & Fill Nullables #65

Merged
merged 4 commits into from
Jan 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/workflows/code-quality.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Code Quality

on:
pull_request:
push:
branches:
- main
tags-ignore:
- '*'

jobs:
scan:
name: Static Analysis with SonarCloud

runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup PHP with Xdebug
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: xdebug

- name: Install dependencies with composer
run: composer update --no-ansi --no-interaction --no-progress

- name: Run tests with pest
run: vendor/bin/pest --coverage-clover coverage.xml

- name: Run PHPStan
run: vendor/bin/phpstan analyse --memory-limit=-1 --error-format=json > phpstan.json

- name: SonarCloud Scan
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

phpstan:
name: Static Anaylsis with PHPStan

runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup PHP with Xdebug
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: xdebug

- name: Install dependencies with composer
run: composer update --no-ansi --no-interaction --no-progress

- name: Run PHPStan
run: vendor/bin/phpstan analyse --memory-limit=-1

types:
name: Type Coverage

runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup PHP with Xdebug
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
coverage: xdebug

- name: Install dependencies with composer
run: composer update --no-ansi --no-interaction --no-progress

- name: Run type coverage with pest
run: vendor/bin/pest --type-coverage --min=98 --memory-limit=-1
42 changes: 0 additions & 42 deletions .github/workflows/sonarcloud.yaml

This file was deleted.

3 changes: 0 additions & 3 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,3 @@ jobs:

- name: Run tests with pest
run: vendor/bin/pest --parallel --coverage --min=80

- name: Run type coverage with pest
run: vendor/bin/pest --type-coverage --min=99 --memory-limit=-1
42 changes: 41 additions & 1 deletion docs/basic-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ $value = MyValue::from('Davey Shafik', 40);

## Type Casting

Bag will cast all values to their defined type _automatically_ for all scalar types, as well as the following:
If the input value matches the type of the property (including [union types](https://www.php.net/manual/en/language.types.type-system.php#language.types.type-system.composite.union)), it will be used as-is. Otherwise, Bag will cast all values to their defined type _automatically_ for all scalar types, as well as the following:

- `Bag` objects
- `\Bag\Collection` and `\Illuminate\Support\Collection` objects
Expand All @@ -77,6 +77,46 @@ Bag will cast all values to their defined type _automatically_ for all scalar ty
> [!TIP]
> We recommend using `\Carbon\CarbonImmutable` for all date times.

## Default Values

You can define default values for your properties by setting them in the constructor:

```php
use Bag\Bag;

readonly class MyValue extends Bag {
public function __construct(
public string $name,
public int $age = 40,
) {
}
}

$value = MyValue::from([
'name' => 'Davey Shafik',
])->toArray(); // ['name' => 'Davey Shafik', 'age' => 40]
```

## Nullable Values

Bag will fill missing nullable values without a default value with `null`:

```php
use Bag\Bag;

readonly class MyValue extends Bag {
public function __construct(
public string $name,
public ?int $age,
) {
}
}

$value = MyValue::from([
'name' => 'Davey Shafik',
])->toArray(); // ['name' => 'Davey Shafik', 'age' => null]
```

### Modifying a Value Object

Value Objects are immutable, so you cannot change their properties directly. Instead, you can create a new instance with the updated values using the `Bag->with()` or `Bag->append()` methods:
Expand Down
6 changes: 6 additions & 0 deletions docs/how-bag-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ start("Bag::from($data)")
--> transform(Transform Input)
--> process(Process Parameters)
--> variadic(Is Variadic?)
--> fillNulls(Fill Nulls)
--> mapInput(Map Input)
--> laravelParams(Laravel Route Parameter Binding)
-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?}
Expand All @@ -51,6 +52,7 @@ click start "https://github.com/dshafik/bag/blob/main/src/Bag/Bag.php" _blank
click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank
click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank
click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank
click fillNull "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillNulls.php" _blank
click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank
click laravelParams "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/LaravelRouteParameters.php" _blank
click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank
Expand Down Expand Up @@ -115,6 +117,7 @@ start("Bag::validate($data)")
--> transform(Transform Input)
--> process(Process Parameters)
--> variadic(Is Variadic?)
--> fillNulls(Fill Nulls)
--> mapInput(Map Input)
-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?}
missingError -- Yes --> errorMissingParameters(MissingPropertiesException)
Expand All @@ -134,6 +137,7 @@ click start "https://github.com/dshafik/bag/blob/main/src/Bag/Concerns/WithValid
click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank
click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank
click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank
click fillNulls "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillNulls.php" _blank
click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank
click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank
click extra "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ExtraParameters.php" _blank
Expand All @@ -151,6 +155,7 @@ start("Bag::from($data)")
--> transform(Transform Input)
--> process(Process Parameters)
--> variadic(Is Variadic?)
--> fillNulls(Fill Nulls)
--> mapInput(Map Input)
--> laravelParams(Laravel Route Parameter Binding)
-- Finalized Input Values --> missing(Missing Parameters) --> missingError{Error?}
Expand All @@ -174,6 +179,7 @@ click start "https://github.com/dshafik/bag/blob/main/src/Bag/Bag.php" _blank
click transform "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/Transform.php" _blank
click process "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/ProcessParameters.php" _blank
click variadic "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/IsVariadic.php" _blank
click fillNulls "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/FillNulls.php" _blank
click mapInput "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MapInput.php" _blank
click laravelParams "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/LaravelRouteParameters.php" _blank
click missing "https://github.com/dshafik/bag/blob/main/src/Bag/Pipelines/Pipes/MissingParameters.php" _blank
Expand Down
11 changes: 6 additions & 5 deletions src/Bag/Attributes/Cast.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
use Bag\Attributes\Attribute as AttributeInterface;
use Bag\Casts\CastsPropertyGet;
use Bag\Casts\CastsPropertySet;
use Illuminate\Support\Collection;
use Bag\Collection;
use Illuminate\Support\Collection as LaravelCollection;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class Cast implements AttributeInterface
Expand All @@ -27,9 +28,9 @@ public function __construct(protected string $casterClassname, mixed ...$paramet
}

/**
* @param Collection<array-key,mixed> $properties
* @param LaravelCollection<array-key,mixed> $properties
*/
public function cast(string $propertyType, string $propertyName, Collection $properties): mixed
public function cast(Collection $propertyType, string $propertyName, LaravelCollection $properties): mixed
{
/** @var CastsPropertySet $cast */
$cast = new $this->casterClassname(...$this->parameters);
Expand All @@ -38,9 +39,9 @@ public function cast(string $propertyType, string $propertyName, Collection $pro
}

/**
* @param Collection<array-key,mixed> $properties
* @param LaravelCollection<array-key,mixed> $properties
*/
public function transform(string $propertyName, Collection $properties): mixed
public function transform(string $propertyName, LaravelCollection $properties): mixed
{
/** @var CastsPropertyGet $cast */
$cast = new $this->casterClassname(...$this->parameters);
Expand Down
7 changes: 4 additions & 3 deletions src/Bag/Attributes/CastInput.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
use Attribute;
use Bag\Attributes\Attribute as AttributeInterface;
use Bag\Casts\CastsPropertySet;
use Illuminate\Support\Collection;
use Bag\Collection;
use Illuminate\Support\Collection as LaravelCollection;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
class CastInput implements AttributeInterface
Expand All @@ -24,9 +25,9 @@ public function __construct(protected string $casterClassname, mixed ...$paramet

/**
* @param class-string<CastsPropertySet> $propertyName
* @param Collection<array-key,mixed> $properties
* @param LaravelCollection<array-key,mixed> $properties
*/
public function cast(string $propertyType, string $propertyName, Collection $properties): mixed
public function cast(Collection $propertyType, string $propertyName, LaravelCollection $properties): mixed
{
/** @var CastsPropertySet $cast */
$cast = new $this->casterClassname(...$this->parameters);
Expand Down
7 changes: 4 additions & 3 deletions src/Bag/Casts/CastsPropertySet.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@

namespace Bag\Casts;

use Illuminate\Support\Collection;
use Bag\Collection;
use Illuminate\Support\Collection as LaravelCollection;

interface CastsPropertySet
{
/**
* @param Collection<array-key,mixed> $properties
* @param LaravelCollection<array-key,mixed> $properties
*/
public function set(string $propertyType, string $propertyName, Collection $properties): mixed;
public function set(Collection $propertyTypes, string $propertyName, LaravelCollection $properties): mixed;
}
9 changes: 7 additions & 2 deletions src/Bag/Casts/CollectionOf.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
namespace Bag\Casts;

use Bag\Bag;
use Bag\Collection;
use Bag\Exceptions\BagNotFoundException;
use Bag\Exceptions\InvalidBag;
use Bag\Exceptions\InvalidCollection;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Collection as LaravelCollection;
use Override;
use ReflectionNamedType;

class CollectionOf implements CastsPropertySet
{
Expand All @@ -29,12 +31,15 @@ public function __construct(public string $valueClassname)
}

/**
* @param class-string<LaravelCollection<array-key,mixed>> $propertyType
* @param Collection<ReflectionNamedType> $propertyTypes
* @param LaravelCollection<array-key,array<array-key, Arrayable<(int|string), mixed>|iterable<(int|string), mixed>|null>> $properties
*/
#[Override]
public function set(string $propertyType, string $propertyName, LaravelCollection $properties): mixed
public function set(Collection $propertyTypes, string $propertyName, LaravelCollection $properties): mixed
{
/** @var class-string<LaravelCollection<array-key,mixed>> $propertyType */
$propertyType = $propertyTypes->first();

if ($propertyType !== LaravelCollection::class && ! \is_subclass_of($propertyType, LaravelCollection::class, true)) {
throw new InvalidCollection(sprintf('The property "%s" must be a subclass of %s', $propertyName, LaravelCollection::class));
}
Expand Down
18 changes: 11 additions & 7 deletions src/Bag/Casts/DateTime.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@

namespace Bag\Casts;

use Bag\Collection;
use Carbon\Exceptions\InvalidFormatException;
use DateMalformedStringException;
use DateTimeImmutable;
use DateTimeInterface;
use Illuminate\Support\Collection;
use Illuminate\Support\Collection as LaravelCollection;
use Override;
use ReflectionNamedType;

/**
* @template T of DateTimeInterface
Expand All @@ -25,9 +27,9 @@ public function __construct(protected string $format = 'Y-m-d H:i:s', protected

#[Override]
/**
* @param Collection<array-key,T> $properties
* @param LaravelCollection<array-key,T> $properties
*/
public function get(string $propertyName, Collection $properties): mixed
public function get(string $propertyName, LaravelCollection $properties): mixed
{
/** @var T $dateTime */
$dateTime = $properties->get($propertyName);
Expand All @@ -36,16 +38,18 @@ public function get(string $propertyName, Collection $properties): mixed
}

/**
* @param class-string<T> $propertyType
* @param Collection<array-key,mixed> $properties
* @param Collection<ReflectionNamedType> $propertyTypes
* @param LaravelCollection<array-key,mixed> $properties
* @return T
* @throws DateMalformedStringException
*/
#[Override]
public function set(string $propertyType, string $propertyName, Collection $properties): mixed
public function set(Collection $propertyTypes, string $propertyName, LaravelCollection $properties): mixed
{
if ($this->dateTimeClass === null) {
$this->dateTimeClass = $propertyType;
/** @var class-string<T> $type */
$type = $propertyTypes->first();
$this->dateTimeClass = $type;
}

$value = $properties->get($propertyName);
Expand Down
Loading
Loading