['Poulet fermier 98%', 'Sel 2%'], ]), portions: $portions ?? new PortionAmount(4.0, 6.0, true), allergens: $allergens ?? [ new Allergen(1, 'Gluten'), new Allergen(2, 'Soja'), ], brand: $brand ?? new Brand( code: new BrandCode('LGL39'), label: 'Le Gaulois', arcadiaId: 'arc-b1', createdAt: new DateTimeImmutable('2024-01-01T00:00:00+00:00'), ), isSeasonal: $isSeasonal, weight: $weight ?? new IncoWeight(1200, 1350, 1250), nutriscore: $nutriscore ?? new Nutriscore('A'), storageCondition: $storageCondition ?? new StorageCondition('Conserver entre 0°C et 4°C'), additives: $additives ?? [ new Additive('E300', 'Acide ascorbique'), ], compo: $compoId !== null ? new IncoCompoId($compoId) : null, createdAt: $createdAt ?? new DateTimeImmutable('2024-01-01T10:00:00+00:00'), updatedAt: $updatedAt ?? new DateTimeImmutable('2024-06-01T12:00:00+00:00'), ); } describe('Inco DTO test', function (): void { it('can instantiate Inco with complete domain object graph', function (): void { // GIVEN $inco = createCompleteInco(); // THEN expect($inco)->toBeInstanceOf(Inco::class) ->and($inco)->toBeInstanceOf(Dto::class) ->and($inco->id)->toBeInstanceOf(IncoId::class) ->and($inco->id->value)->toBe('100') ->and($inco->name)->toBe('Poulet fermier') ->and($inco->libelle)->toBe('Poulet fermier Label Rouge') ->and($inco->description)->toBe('Poulet élevé en plein air 1.2kg') ->and($inco->sellArguments)->toBe('Origine France') ->and($inco->preparationAdvices)->toBe('Cuire au four à 180°C pendant 1h15') ->and($inco->tips)->toBe('Arroser régulièrement') ->and($inco->sellFamilly)->toBe('Volaille') ->and($inco->isSeasonal)->toBeTrue() ->and($inco->compo)->toBeInstanceOf(IncoCompoId::class) ->and($inco->compo?->value)->toBe('200'); // Sub-DTOs: Allegations expect($inco->allegations)->toBeInstanceOf(Allegations::class) ->and($inco->allegations->allegations())->toBe(['Sans OGM']) ->and($inco->allegations->health())->toBe(['Riche en protéines']) ->and($inco->allegations->nutrition())->toBe(['Faible en matières grasses']) ->and($inco->allegations->other())->toBe(['Label Rouge']); // Sub-DTOs: Ingredients expect($inco->ingredients)->toBeInstanceOf(Ingredients::class) ->and($inco->ingredients->groups)->toBe(['groupe1' => ['Poulet fermier 98%', 'Sel 2%']]) ->and($inco->ingredients->flatten())->toBe(['Poulet fermier 98%', 'Sel 2%']) ->and($inco->ingredients->isEmpty())->toBeFalse(); // Sub-DTOs: Portions expect($inco->portions)->toBeInstanceOf(PortionAmount::class) ->and($inco->portions?->getLowValue())->toBe(4.0) ->and($inco->portions?->getHighValue())->toBe(6.0) ->and($inco->portions?->getType())->toBe(PortionType::approximative); // Sub-DTOs: Allergens expect($inco->allergens)->toHaveCount(2) ->and($inco->allergens[0])->toBeInstanceOf(Allergen::class) ->and($inco->allergens[0]->code)->toBe(1) ->and($inco->allergens[0]->label)->toBe('Gluten') ->and($inco->allergens[1]->code)->toBe(2) ->and($inco->allergens[1]->label)->toBe('Soja'); // Sub-DTOs: Brand expect($inco->brand)->toBeInstanceOf(Brand::class) ->and($inco->brand->code->value)->toBe('LGL39') ->and($inco->brand->label)->toBe('Le Gaulois'); // Sub-DTOs: Weight expect($inco->weight)->toBeInstanceOf(IncoWeight::class) ->and($inco->weight?->netUvc)->toBe(1200) ->and($inco->weight?->brutUvc)->toBe(1350) ->and($inco->weight?->moyTheo)->toBe(1250); // Sub-DTOs: Nutriscore expect($inco->nutriscore)->toBeInstanceOf(Nutriscore::class) ->and($inco->nutriscore?->grade)->toBe('A'); // Sub-DTOs: StorageCondition expect($inco->storageCondition)->toBeInstanceOf(StorageCondition::class) ->and($inco->storageCondition?->instructions)->toBe('Conserver entre 0°C et 4°C'); // Sub-DTOs: Additives expect($inco->additives)->toHaveCount(1) ->and($inco->additives[0])->toBeInstanceOf(Additive::class) ->and($inco->additives[0]->code)->toBe('E300') ->and($inco->additives[0]->label)->toBe('Acide ascorbique'); }); it('can jsonSerialize complete Inco graph', function (): void { // GIVEN $inco = createCompleteInco(); // WHEN $serialized = $inco->jsonSerialize(); // THEN expect($serialized)->toBeArray() ->and($serialized['id'])->toBe('100') ->and($serialized['name'])->toBe('Poulet fermier') ->and($serialized['libelle'])->toBe('Poulet fermier Label Rouge') ->and($serialized['description'])->toBe('Poulet élevé en plein air 1.2kg') ->and($serialized['sellArguments'])->toBe('Origine France') ->and($serialized['preparationAdvices'])->toBe('Cuire au four à 180°C pendant 1h15') ->and($serialized['tips'])->toBe('Arroser régulièrement') ->and($serialized['sellFamilly'])->toBe('Volaille') ->and($serialized['isSeasonal'])->toBeTrue() ->and($serialized['compo'])->toBe('200') ->and($serialized['brand'])->toBeArray() ->and($serialized['brand']['label'])->toBe('Le Gaulois') ->and($serialized['brand']['code'])->toBeInstanceOf(BrandCode::class); // Recursive json_encode check $jsonString = json_encode($inco); expect($jsonString)->toBeString(); $decoded = json_decode((string) $jsonString, true); expect($decoded)->toBeArray() ->and($decoded['id'])->toBe('100') ->and($decoded['brand']['label'])->toBe('Le Gaulois') ->and($decoded['brand']['code']['value'])->toBe('LGL39') ->and($decoded['allergens'])->toHaveCount(2) ->and($decoded['allergens'][0]['label'])->toBe('Gluten') ->and($decoded['additives'])->toHaveCount(1) ->and($decoded['additives'][0]['code'])->toBe('E300') ->and($decoded['nutriscore'])->toBe('A') ->and($decoded['storageCondition'])->toBe('Conserver entre 0°C et 4°C') ->and($decoded['weight']['netUvc'])->toBe(1200); }); it('can instantiate Inco with minimal nullable fields', function (): void { // GIVEN / WHEN $inco = new Inco( id: new IncoId('101'), name: 'Minimal', libelle: 'Minimal Label', description: 'Minimal Description', allegations: new Allegations(), sellArguments: '', preparationAdvices: '', tips: null, sellFamilly: null, ingredients: new Ingredients([]), portions: null, allergens: [], brand: new Brand(new BrandCode('LGL39'), 'Le Gaulois'), isSeasonal: false, weight: null, nutriscore: null, storageCondition: null, additives: [], compo: null, createdAt: null, updatedAt: null, ); // THEN expect($inco->tips)->toBeNull() ->and($inco->sellFamilly)->toBeNull() ->and($inco->portions)->toBeNull() ->and($inco->allergens)->toBeEmpty() ->and($inco->weight)->toBeNull() ->and($inco->nutriscore)->toBeNull() ->and($inco->storageCondition)->toBeNull() ->and($inco->additives)->toBeEmpty() ->and($inco->compo)->toBeNull() ->and($inco->createdAt)->toBeNull() ->and($inco->updatedAt)->toBeNull(); $serialized = $inco->jsonSerialize(); expect($serialized['portions'])->toBeNull() ->and($serialized['weight'])->toBeNull() ->and($serialized['nutriscore'])->toBeNull() ->and($serialized['storageCondition'])->toBeNull() ->and($serialized['compo'])->toBeNull(); }); it('throws InvalidArgumentException if allergens array contains invalid element', function (): void { // GIVEN $invalidAllergens = ['invalid string']; // THEN $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Allergens shall be instance of ' . Allergen::class); // WHEN new Inco( id: new IncoId('100'), name: 'Poulet', libelle: 'Poulet', description: 'Poulet', allegations: new Allegations(), sellArguments: '', preparationAdvices: '', tips: null, sellFamilly: null, ingredients: new Ingredients([]), portions: null, // @phpstan-ignore argument.type allergens: $invalidAllergens, brand: new Brand(new BrandCode('LGL39'), 'Le Gaulois'), isSeasonal: false, weight: null, nutriscore: null, storageCondition: null, additives: [], compo: null, createdAt: null, updatedAt: null, ); }); it('throws InvalidArgumentException if additives array contains invalid element', function (): void { // GIVEN $invalidAdditives = [new \stdClass()]; // THEN $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Additives shall be instance of ' . Additive::class); // WHEN new Inco( id: new IncoId('100'), name: 'Poulet', libelle: 'Poulet', description: 'Poulet', allegations: new Allegations(), sellArguments: '', preparationAdvices: '', tips: null, sellFamilly: null, ingredients: new Ingredients([]), portions: null, allergens: [], brand: new Brand(new BrandCode('LGL39'), 'Le Gaulois'), isSeasonal: false, weight: null, nutriscore: null, storageCondition: null, // @phpstan-ignore argument.type additives: $invalidAdditives, compo: null, createdAt: null, updatedAt: null, ); }); });