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
4 changes: 4 additions & 0 deletions docs/reference/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -853,6 +853,10 @@ A <code>@OA\Request</code> path parameter.
<dl>
<dt><strong>property</strong> : <span style="font-family: monospace;">string</span></dt>
<dd><p>The key into Schema->properties array.</p><table class="table-plain"><tbody><tr><td><i>Required</i>:</td><td style="padding-left: 0;"><b>no</b></td></tr></tbody></table></dd>
<dt><strong>required</strong> : <span style="font-family: monospace;">bool|list&lt;string&gt;</span></dt>
<dd><p>A boolean flags the property as required in its parent schema, or a list names the required members when it is an object.<br />
<br />
The <code>AugmentRequired</code> processor collects a boolean into the parent <code>Schema::$required</code>.</p><table class="table-plain"><tbody><tr><td><i>Required</i>:</td><td style="padding-left: 0;"><b>no</b></td></tr></tbody></table></dd>
</dl>

### [Put](https://github.com/zircote/swagger-php/tree/master/src/Annotations/Put.php)
Expand Down
7 changes: 4 additions & 3 deletions docs/reference/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2238,9 +2238,10 @@ value of this attribute.</p><table class="table-plain"><tbody><tr><td><i>Require
<dd><p>The minimum number of properties allowed in an object instance.<br />
An object instance is valid against this property if its number of properties is greater than, or equal to, the<br />
value of this attribute.</p><table class="table-plain"><tbody><tr><td><i>Required</i>:</td><td style="padding-left: 0;"><b>no</b></td></tr></tbody></table></dd>
<dt><strong>required</strong> : <span style="font-family: monospace;">list&lt;string&gt;</span></dt>
<dd><p>An object instance is valid against this property if its property set contains all elements in this property's<br />
array value.</p><table class="table-plain"><tbody><tr><td><i>Required</i>:</td><td style="padding-left: 0;"><b>no</b></td></tr></tbody></table></dd>
<dt><strong>required</strong> : <span style="font-family: monospace;">bool|list&lt;string&gt;</span></dt>
<dd><p>A boolean flags the property as required in its parent schema, or a list names the required members when it is an object.<br />
<br />
The <code>AugmentRequired</code> processor collects a boolean into the parent <code>Schema::$required</code>.</p><table class="table-plain"><tbody><tr><td><i>Required</i>:</td><td style="padding-left: 0;"><b>no</b></td></tr></tbody></table></dd>
<dt><strong>properties</strong> : <span style="font-family: monospace;">list&lt;Property&gt;</span></dt>
<dd><p>A collection of properties to define for an object.<br />
<br />
Expand Down
21 changes: 21 additions & 0 deletions docs/reference/processors.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,27 @@ Use the property context to extract useful information and inject that into the



### [AugmentRequired](https://github.com/zircote/swagger-php/tree/master/src/Processors/AugmentRequired.php)

Populate <code>Schema::$required</code> from its properties.

A property with a boolean <code>required</code> value is always honoured; <code>true</code> adds it to the parent <code>Schema::$required</code> and <code>false</code> removes it.
The boolean is consumed so it never serialises on the property itself.
When the <code>augmentRequired.enabled</code> config flag is set, properties without an explicit boolean are inferred: a property backed by a PHP member (property, promoted parameter or method) becomes required when it has a known, non-nullable type.
Nullable properties, and properties whose type cannot be determined, are left optional.
Inference is skipped for a schema that already declares a <code>required</code> list, leaving that list as-is.

Inference (the <code>augmentRequired.enabled</code> flag) is off by default; the boolean <code>required</code> handling always runs.
#### Config settings
**augmentRequired.enabled**
: <span style="font-family: monospace;">bool</span>
<br>**default**
: <span style="font-family: monospace;">false</span>

&nbsp;&nbsp;&nbsp;&nbsp;Enables/disables the <code>AugmentRequired</code> processor.<br>



### [AugmentDiscriminators](https://github.com/zircote/swagger-php/tree/master/src/Processors/AugmentDiscriminators.php)

Use the property context to extract useful information and inject that into the annotation.
Expand Down
9 changes: 9 additions & 0 deletions src/Annotations/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ class Property extends Schema
*/
public $property = Generator::UNDEFINED;

/**
* A boolean flags the property as required in its parent schema, or a list names the required members when it is an object.
*
* The <code>AugmentRequired</code> processor collects a boolean into the parent <code>Schema::$required</code>.
*
* @var bool|list<string>
*/
public $required = Generator::UNDEFINED; // @phpstan-ignore property.phpDocType

/**
* @var Encoding
*/
Expand Down
4 changes: 2 additions & 2 deletions src/Attributes/Property.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Property extends OA\Property
{
/**
* @param string|class-string|object|null $ref
* @param list<string> $required
* @param bool|list<string> $required
* @param list<Property> $properties
* @param string|non-empty-array<string>|null $type
* @param array<Examples> $examples
Expand All @@ -36,7 +36,7 @@ public function __construct(
?string $description = Generator::UNDEFINED,
?int $maxProperties = null,
?int $minProperties = null,
?array $required = null,
bool|array|null $required = null,
?array $properties = null,
string|array|null $type = null,
?string $format = null,
Expand Down
4 changes: 4 additions & 0 deletions src/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ public function getDefaultConfig(): array
'augmentParameters' => [
'augmentOperationParameters' => true,
],
'augmentRequired' => [
'enabled' => false,
],
'pathFilter' => [
'tags' => [],
'paths' => [],
Expand Down Expand Up @@ -249,6 +252,7 @@ public function getProcessorPipeline(): Pipeline
new Processors\AugmentSchemas(),
new Processors\AugmentRequestBody(),
new Processors\AugmentProperties(),
new Processors\AugmentRequired(),
new Processors\AugmentDiscriminators(),
new Processors\BuildPaths(),
new Processors\AugmentParameters(),
Expand Down
148 changes: 148 additions & 0 deletions src/Processors/AugmentRequired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Processors;

use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;

/**
* Populate <code>Schema::$required</code> from its properties.
*
* A property with a boolean <code>required</code> value is always honoured; <code>true</code> adds it to the parent <code>Schema::$required</code> and <code>false</code> removes it.
* The boolean is consumed so it never serialises on the property itself.
* When the <code>augmentRequired.enabled</code> config flag is set, properties without an explicit boolean are inferred: a property backed by a PHP member (property, promoted parameter or method) becomes required when it has a known, non-nullable type.
* Nullable properties, and properties whose type cannot be determined, are left optional.
* Inference is skipped for a schema that already declares a <code>required</code> list, leaving that list as-is.
*
* Inference (the <code>augmentRequired.enabled</code> flag) is off by default; the boolean <code>required</code> handling always runs.
*/
class AugmentRequired
{
protected bool $enabled;

public function __construct(bool $enabled = false)
{
$this->enabled = $enabled;
}

public function isEnabled(): bool
{
return $this->enabled;
}

/**
* Enables/disables the <code>AugmentRequired</code> processor.
*/
public function setEnabled(bool $enabled): AugmentRequired
{
$this->enabled = $enabled;

return $this;
}

public function __invoke(Analysis $analysis): void
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class);

foreach ($schemas as $schema) {
if (!is_array($schema->properties) || $schema->properties === []) {
continue;
}

$declared = !Generator::isDefault($schema->required);
$required = $declared ? $schema->required : [];
$changed = false;

foreach ($schema->properties as $property) {
// consume an explicit boolean regardless of the property name so it never serialises on the property
$explicit = is_bool($property->required) ? $property->required : null;
if (null !== $explicit) {
/* @phpstan-ignore assign.propertyType */
$property->required = Generator::UNDEFINED;
}

if (Generator::isDefault($property->property) || !is_string($property->property)) {
continue;
}
$name = $property->property;

// an explicit boolean is always honoured: true adds the property, false removes it
if (null !== $explicit) {
if (null !== $updated = $this->withRequired($required, $name, $explicit)) {
$required = $updated;
$changed = true;
}

continue;
}

// inference is opt-in and never touches an explicitly declared list
if (!$this->enabled || $declared) {
continue;
}

if ($this->isInferredRequired($property) && null !== $updated = $this->withRequired($required, $name, true)) {
$required = $updated;
$changed = true;
}
}

if ($changed) {
$schema->required = $required === [] ? Generator::UNDEFINED : $required;
}
}
}

/**
* Whether an unflagged property should be inferred as required.
*/
protected function isInferredRequired(OA\Property $property): bool
{
// only infer from properties backed by a PHP member
$reflector = $property->_context->reflector;
if (!$reflector instanceof \ReflectionProperty
&& !$reflector instanceof \ReflectionParameter
&& !$reflector instanceof \ReflectionMethod) {
return false;
}

if ($property->isNullable()) {
return false;
}

// only mark required when a non-nullable type is actually known
return !Generator::isDefault($property->type) || !Generator::isDefault($property->ref);
}

/**
* Add or remove a property name in the required list.
*
* @param list<string> $required
*
* @return list<string>|null the updated list, or null when nothing changed
*/
protected function withRequired(array $required, string $name, bool $include): ?array
{
$pos = array_search($name, $required, true);

if ($include && false === $pos) {
$required[] = $name;

return $required;
}

if (!$include && false !== $pos) {
unset($required[$pos]);

return array_values($required);
}

return null;
}
}
2 changes: 1 addition & 1 deletion tests/Analysers/AttributeAnnotationFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public function testErrorOnInvalidAttribute(): void
$rm = new \ReflectionMethod($instance, 'post');

$this->expectException(\TypeError::class);
$this->expectExceptionMessage(Property::class . '::__construct(): Argument #9 ($required) must be of type ?array');
$this->expectExceptionMessage(Property::class . '::__construct(): Argument #9 ($required) must be of type array|bool|null');

(new AttributeAnnotationFactory())->build($rm, $this->getContext());
}
Expand Down
19 changes: 19 additions & 0 deletions tests/Fixtures/AllNullableProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Tests\Fixtures;

use OpenApi\Attributes as OAT;

#[OAT\Schema]
class AllNullableProperties
{
#[OAT\Property]
public ?string $a;

#[OAT\Property]
public ?int $b;
}
16 changes: 16 additions & 0 deletions tests/Fixtures/DeclaredRequiredEmptied.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Tests\Fixtures;

use OpenApi\Attributes as OAT;

#[OAT\Schema(schema: 'DeclaredRequiredEmptied', required: ['only'])]
class DeclaredRequiredEmptied
{
#[OAT\Property(required: false)]
public string $only;
}
19 changes: 19 additions & 0 deletions tests/Fixtures/DeclaredRequiredMerged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Tests\Fixtures;

use OpenApi\Attributes as OAT;

#[OAT\Schema(schema: 'DeclaredRequiredMerged', required: ['declared'])]
class DeclaredRequiredMerged
{
#[OAT\Property]
public string $declared;

#[OAT\Property(required: true)]
public string $flagged;
}
22 changes: 22 additions & 0 deletions tests/Fixtures/ExplicitRequired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Tests\Fixtures;

use OpenApi\Attributes as OAT;

#[OAT\Schema]
class ExplicitRequired
{
#[OAT\Property(required: true)]
public ?string $explicitlyRequired;

#[OAT\Property(required: false)]
public string $explicitlyOptional;

#[OAT\Property]
public ?string $untouched;
}
20 changes: 20 additions & 0 deletions tests/Fixtures/InlineProperties.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Tests\Fixtures;

use OpenApi\Attributes as OAT;

#[OAT\Schema(
schema: 'InlineProperties',
properties: [
new OAT\Property(property: 'inlineTyped', type: 'string'),
new OAT\Property(property: 'inlineFlagged', type: 'string', required: true),
]
)]
class InlineProperties
{
}
2 changes: 1 addition & 1 deletion tests/Fixtures/InvalidPropertyAttribute.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

class InvalidPropertyAttribute
{
#[OA\Property(required: true)] // required has to be array or null
#[OA\Property(required: 'yes')] // required has to be a bool, array or null
public function post()
{
}
Expand Down
22 changes: 22 additions & 0 deletions tests/Fixtures/ObjectPropertyWithRequired.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types=1);

/**
* @license Apache 2.0
*/

namespace OpenApi\Tests\Fixtures;

use OpenApi\Attributes as OAT;

#[OAT\Schema]
class ObjectPropertyWithRequired
{
#[OAT\Property(
property: 'address',
properties: [
new OAT\Property(property: 'street', type: 'string'),
],
required: ['street'],
)]
public $address;
}
Loading