Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:

strategy:
matrix:
php-version: [8.1, 8.2, 8.3] # opcional, se quiser testar várias versões
php-version: [8.1, 8.2, 8.3]

steps:
- name: Checkout repository
Expand Down
34 changes: 34 additions & 0 deletions check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/bin/bash
set -e

offer_run() {
read -p "For more output, run \"$1\". Run it now (Y/n)? " run

case ${run:0:1} in
n|N )
exit 1
;;
* )
eval "$1"
;;
esac

exit 1
}

if (./vendor/bin/phpstan analyse --memory-limit=1G > /dev/null 2>/dev/null); then
echo '✅ PHPStan OK'
else
echo '❌ PHPStan FAILED'
offer_run "./vendor/bin/phpstan analyse --memory-limit=1G"
fi

if (./vendor/bin/phpunit --colors=always > /dev/null 2>/dev/null); then
echo '✅ PHPUnit OK'
else
echo '❌ PHPUnit FAILED'
offer_run "./vendor/bin/phpunit --colors=always"
fi

echo '=============================='
echo '✅ All checks passed (local CI)'
22 changes: 19 additions & 3 deletions src/Model/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,19 +101,35 @@ public function setSchemaName(string $name): self
*/
public function fill(array $data): static
{
$snapshot = $this->copy();
$snapshot->mutators = [];

foreach ($data as $key => $value) {
$this->set($key, $value);
$snapshot->set($key, $value);
}

foreach ($this->schema->getAttributes() as $attr) {
if ($attr->isRequired() && !array_key_exists($attr->getName(), $data)) {
foreach ($snapshot->schema->getAttributes() as $attr) {
/** @var SchemaAttribute $attr */
if (
($attr->isRequired() || $attr->isRequiredIf($snapshot->get($attr->getName()), $snapshot))
&& !array_key_exists($attr->getName(), $data)
) {
throw new SchemaAttributeParseException($attr, "Missing required attribute");
}
}

$this->data = $snapshot->data;
$this->relations = $snapshot->relations;
$this->mutators = $snapshot->mutators;

return $this;
}

public function copy(): static
{
return clone $this;
}

public function set(string $attribute, mixed $value): self
{
$attributeSchema = $this->schema->query($attribute);
Expand Down
18 changes: 18 additions & 0 deletions src/Model/Schema/SchemaAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class SchemaAttribute
protected mixed $default;
protected bool $hasDefault;
protected bool $required;
protected ?Closure $requiredCheck;

public function __construct(Schema $schema, string $name)
{
Expand All @@ -31,6 +32,7 @@ public function __construct(Schema $schema, string $name)
$this->hasDefault = false;

$this->required = false;
$this->requiredCheck = null;
}

public function getName(): string
Expand Down Expand Up @@ -84,6 +86,11 @@ public function isHiddenIf(mixed $value, Model $model): bool
return $this->hiddenCheck?->__invoke($value, $model) ?? false;
}

public function isRequiredIf(mixed $value, Model $model): bool
{
return $this->requiredCheck?->__invoke($value, $model) ?? false;
}

public function hasDefault(): bool
{
return $this->hasDefault;
Expand Down Expand Up @@ -133,6 +140,17 @@ public function required(bool $default = true): self
return $this;
}

public function requiredIf(callable $check): self
{
$this->requiredCheck = $check instanceof Closure ? $check : Closure::fromCallable($check);
return $this;
}

public function requiredWhen(string $field, mixed $expectedValue): self
{
return $this->requiredIf(static fn($value, Model $model) => $model->get($field) === $expectedValue);
}

public function array(): SchemaArrayAttribute
{
return $this->schema->array($this->getName(), $this);
Expand Down
222 changes: 218 additions & 4 deletions tests/Model/ComprehensiveModelTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

namespace IpagDevs\Tests;

use Exception;
use DateTimeInterface;
use ReflectionProperty;
use IpagDevs\Model\Model;
use IpagDevs\Model\Schema\Schema;
use IpagDevs\Model\Schema\Mutator;
Expand Down Expand Up @@ -89,6 +91,34 @@ public function slug(): Mutator
}
}

class UserRegistration extends Model
{
protected function schema(SchemaBuilder $schema): Schema
{
$schema->string('name')->required();
$schema->string('email')->required();
$schema->string('user_type')->required();
$schema->string('contact_preference')->nullable();

$schema->string('company_name')
->requiredWhen('user_type', 'business');

$schema->string('phone')
->requiredWhen('contact_preference', 'phone');

$schema->string('supervisor_approval')
->requiredIf(function ($value, Model $model) {
$budget = $model->get('budget');
$userType = $model->get('user_type');
return $budget > 10000 && $userType !== 'enterprise';
});

$schema->float('budget')->nullable();

return $schema->build();
}
}

class ComprehensiveModelTest extends BaseTestCase
{
/**
Expand Down Expand Up @@ -125,12 +155,188 @@ protected function setUp(): void

protected function tearDown(): void
{
$reflection = new \ReflectionProperty(Model::class, 'globalSchema');
$reflection->setAccessible(true);
$reflection = new ReflectionProperty(Model::class, 'globalSchema');
$reflection->setValue(null, []);
parent::tearDown();
}

public function testRequiredWhenConditionIsTriggeredAndFieldMissing(): void
{
$this->expectException(SchemaAttributeParseException::class);
$this->expectExceptionMessage("Missing required attribute");

$data = [
'name' => 'João',
'email' => 'joao@test.com',
'user_type' => 'business',
];

UserRegistration::parse($data);
}

public function testRequiredWhenConditionNotTriggeredAllowsMissingField(): void
{
$data = [
'name' => 'Maria',
'email' => 'maria@test.com',
'user_type' => 'personal',
];

$user = UserRegistration::parse($data);

$this->assertNull($user->get('company_name'));
}

public function testPhoneIsRequiredWhenContactPreferenceIsPhone(): void
{
$data = [
'name' => 'Ana',
'email' => 'ana@test.com',
'user_type' => 'personal',
'contact_preference' => 'phone',
'phone' => '11999999999'
];

$user = UserRegistration::parse($data);

$this->assertSame('11999999999', $user->get('phone'));
}

public function testMissingPhoneThrowsWhenConditionIsTriggered(): void
{
$this->expectException(SchemaAttributeParseException::class);
$this->expectExceptionMessage("Missing required attribute");

$data = [
'name' => 'Ana',
'email' => 'ana@test.com',
'user_type' => 'personal',
'contact_preference' => 'phone',
];

UserRegistration::parse($data);
}

public function testPhoneNotRequiredWhenConditionIsNotMet(): void
{
$data = [
'name' => 'Ana',
'email' => 'ana@test.com',
'user_type' => 'personal',
'contact_preference' => 'email',
];

$user = UserRegistration::parse($data);
$this->assertNull($user->get('phone'));
}

public function testSupervisorApprovalNotRequiredWhenBudgetLow(): void
{
$data = [
'name' => 'Carlos',
'email' => 'carlos@test.com',
'user_type' => 'business',
'budget' => 5000,
'company_name' => 'Negócios SA'
];

$user = UserRegistration::parse($data);

$this->assertNull($user->get('supervisor_approval'));
}
public function testSupervisorApprovalNotRequiredForEnterpriseUsers(): void
{
$data = [
'name' => 'CEO',
'email' => 'ceo@test.com',
'user_type' => 'enterprise',
'budget' => 30000
];

$user = UserRegistration::parse($data);

$this->assertNull($user->get('supervisor_approval'));
}

public function testSupervisorApprovalRequiredWhenBudgetHighAndUserNotEnterprise(): void
{
$this->expectException(SchemaAttributeParseException::class);
$this->expectExceptionMessage("Missing required attribute");

$data = [
'name' => 'Gerente',
'email' => 'gerente@test.com',
'user_type' => 'business',
'budget' => 20000,
];

UserRegistration::parse($data);
}
public function testSupervisorApprovalIsAcceptedWhenProvided(): void
{
$data = [
'name' => 'Gerente',
'email' => 'gerente@test.com',
'user_type' => 'business',
'budget' => 20000,
'supervisor_approval' => 'Aprovado por João',
'company_name' => 'Negócios SA'
];

$user = UserRegistration::parse($data);

$this->assertSame('Aprovado por João', $user->get('supervisor_approval'));
}
public function testRequiredIfFailsWhenValueIsExplicitNull(): void
{
$this->expectException(SchemaAttributeParseException::class);

$data = [
'name' => 'Pedro',
'email' => 'pedro@test.com',
'user_type' => 'business',
'budget' => 15000,
'supervisor_approval' => null
];

UserRegistration::parse($data);
}

public function testFillUsesSnapshotAndDoesNotModifyOriginalOnFailure(): void
{
$instance = new class extends Model {
protected function schema(SchemaBuilder $schema): Schema
{
$schema->int('id')->required();
$schema->string('name')->between(3, 10)->required();
return $schema->build();
}
};

$class = $instance::class;

$model = $class::parse([
'id' => 1,
'name' => 'ValidName'
]);

$this->assertSame(1, $model->get('id'));
$this->assertSame('ValidName', $model->get('name'));

$this->expectException(SchemaAttributeParseException::class);

try {
$model->fill([
'id' => 2,
]);
} catch (Exception $e) {
$this->assertSame(1, $model->get('id'));
$this->assertSame('ValidName', $model->get('name'));

throw $e;
}
}

public function testModelHiddenIf(): void
{
$review = Review::parse([
Expand Down Expand Up @@ -289,12 +495,20 @@ public function testHiddenIfWithComplexCondition(): void
{
$highPriceData = $this->fullProductData;
$product1 = Product::parse($highPriceData);
$this->assertArrayNotHasKey('promo_code', $product1->jsonSerialize(), "Promo code should be hidden for high-priced items.");
$this->assertArrayNotHasKey(
'promo_code',
$product1->jsonSerialize(),
"Promo code should be hidden for high-priced items."
);

$lowPriceData = $this->fullProductData;
$lowPriceData['price'] = 50.00;
$product2 = Product::parse($lowPriceData);
$this->assertArrayHasKey('promo_code', $product2->jsonSerialize(), "Promo code should be visible for low-priced items.");
$this->assertArrayHasKey(
'promo_code',
$product2->jsonSerialize(),
"Promo code should be visible for low-priced items."
);
$this->assertSame('SAVE10', $product2->jsonSerialize()['promo_code']);
}

Expand Down