1. Go to this page and download the library: Download vanilla/garden-schema library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
[
'<property>', // basic property, can be any type
'<property>?', // optional property
'<property>:<type>?', // optional property with specific type
'<property>:<type>?' => 'Description', // optional, typed property with description
'<property>?' => ['type' => '<type'>, 'description' => '...'], // longer format
'<property>:o' => [ // object property with nested schema
'<property>:<type>' => '...',
...
],
'<property>:a' => '<type>', // array property with element type
'<property>:a' => [ // array property with object element type
'<property>:<type>' => '...',
...
]
]
$schema = Schema::parse([
'items:a', // array of any type
'tags:a' => 's', // array of strings
'attributes:o', // object of any type
'user:o' => [ // an object with specific properties
'name:s',
'email:s?'
]
]);
$userSchema = Schema::parse([
'name:s',
'email:s?'
]);
$recordSchema = Schema::parse([
"uuid:s",
"body:s",
// User schema is userSchema,
]);
$recordSchema = Schema::parse([
"uuid:s",
"body:s",
// Array of user schema objects.
"users:a" => $userSchema,
]);
use Garden\Schema\Entity;
class User extends Entity {
public string $name;
public string $email;
public int $age;
public ?string $bio = null; // Optional, nullable with default
}
// Get the schema
$schema = User::getSchema();
// Validate and create an entity
$user = User::from([
'name' => 'John',
'email' => '[email protected]',
'age' => 30,
]);
$user->name; // 'John'
$user->age; // 30 (integer)
use Garden\Schema\Entity;
use Garden\Schema\PropertySchema;
class Article extends Entity {
public string $title;
// Add constraints to auto-generated string type
#[PropertySchema(['minLength' => 10, 'maxLength' => 5000])]
public string $body;
// Add items constraint to auto-generated array type
#[PropertySchema(['items' => ['type' => 'string']])]
public array $tags;
// Multiple constraints
#[PropertySchema(['items' => ['type' => 'string'], 'minItems' => 1])]
public array $categories;
}
use Garden\Schema\Entity;
use Garden\Schema\Required;
class User extends Entity {
public string $name;
// Nullable but lic ?string $bio = null;
}
// This throws ValidationException - nickname is e' => 'Johnny']);
use Garden\Schema\Entity;
use Garden\Schema\PropertyAltNames;
class User extends Entity {
// Single alt name - primaryAltName is inferred
#[PropertyAltNames('user_name')]
public string $name;
// Multiple alt names - primaryAltName is ']); // Alt name
// Main property name takes precedence
$user3 = User::from(['name' => 'Main', 'user_name' => 'Alt']);
$user3->name; // 'Main'
use Garden\Schema\Entity;
use Garden\Schema\PropertyAltNames;
class Config extends Entity {
#[PropertyAltNames(
['settings.displayName', 'meta.name', 'name'],
primaryAltName: 'settings.displayName'
)]
public string $displayName;
}
// All of these work:
$config1 = Config::from(['displayName' => 'Direct']);
$config2 = Config::from(['settings' => ['displayName' => 'Nested']]);
$config3 = Config::from(['meta' => ['name' => 'Deep Nested']]);
$config4 = Config::from(['name' => 'Fallback']);
// Disable dot notation if you need literal dots in property names
class LiteralDots extends Entity {
#[PropertyAltNames(['some.literal.key'], useDotNotation: false)]
public string $value;
}
use Garden\Schema\EntityFieldFormat;
// Convert a single field name
User::convertFieldName('name', EntityFieldFormat::PrimaryAltName); // 'user_name'
User::convertFieldName('user_name', EntityFieldFormat::Canonical); // 'name'
// Fields without alt names are returned unchanged
User::convertFieldName('age', EntityFieldFormat::PrimaryAltName); // 'age'
// Convert multiple field names at once
User::convertFieldNames(
['name', 'email', 'age'],
EntityFieldFormat::PrimaryAltName
);
// ['user_name', 'e-mail', 'age']
// Round-trip conversion
$alt = User::convertFieldName('name', EntityFieldFormat::PrimaryAltName); // 'user_name'
$canonical = User::convertFieldName($alt, EntityFieldFormat::Canonical); // 'name'
use Garden\Schema\Entity;
use Garden\Schema\MapSubProperties;
class Author extends Entity {
public int $authorID;
public string $authorName;
public ?string $email = null;
public ?string $bio = null;
}
class Article extends Entity {
public int $articleID;
public string $title;
#[MapSubProperties(
keys: ['authorID', 'authorName'],
mapping: ['metadata.authorEmail' => 'email', 'metadata.authorBio' => 'bio']
)]
public Author $author;
}
// Input with flat structure and nested metadata:
$article = Article::from([
'articleID' => 1,
'title' => 'My Article',
'authorID' => 123,
'authorName' => 'John Doe',
'metadata' => [
'authorEmail' => '[email protected]',
'authorBio' => 'A prolific writer',
],
]);
// The author property is automatically populated:
$article->author->authorID; // 123
$article->author->authorName; // 'John Doe'
$article->author->email; // '[email protected]'
$article->author->bio; // 'A prolific writer'
#[MapSubProperties(
keys: ['user.id', 'user.name'], // Copy nested source to same path in target
mapping: [
'profile.avatar' => 'image.url', // Remap nested source to different target path
'settings.theme' => 'preferences.theme'
]
)]
public \ArrayObject $userData;
use Garden\Schema\Entity;
use Garden\Schema\ExcludeFromSchema;
class Article extends Entity {
public string $title;
public string $body;
#[ExcludeFromSchema]
public string $slug = ''; // Computed from title
#[ExcludeFromSchema]
public ?array $cache = null; // Internal cache
}
$article = Article::from([
'title' => 'Hello World',
'body' => 'Content here',
'slug' => 'ignored', // This is ignored during validation
]);
$article->title; // 'Hello World'
$article->slug; // '' (default value, input was ignored)
// Excluded properties can still be set directly
$article->slug = 'hello-world';
$article->cache = ['rendered' => '<p>Content here</p>'];
// Excluded properties are not
use Garden\Schema\SchemaVariant;
// Get different schema variants
$fullSchema = Article::getSchema(); // Default: Full
$fullSchema = Article::getSchema(SchemaVariant::Full); // Explicit Full
$fragmentSchema = Article::getSchema(SchemaVariant::Fragment);
$mutableSchema = Article::getSchema(SchemaVariant::Mutable);
$createSchema = Article::getSchema(SchemaVariant::Create);
$internalSchema = Article::getSchema(SchemaVariant::Internal);
use Garden\Schema\Entity;
use Garden\Schema\ExcludeFromVariant;
use Garden\Schema\SchemaVariant;
class Article extends Entity {
public int $id;
public string $title;
// Exclude from Fragment (too large for list responses)
#[ExcludeFromVariant(SchemaVariant::Fragment)]
public string $body;
// Exclude from Mutable (system-managed, not user-editable)
#[ExcludeFromVariant(SchemaVariant::Mutable)]
public \DateTimeImmutable $createdAt;
#[ExcludeFromVariant(SchemaVariant::Mutable)]
public \DateTimeImmutable $updatedAt;
// Exclude from multiple variants
#[ExcludeFromVariant(SchemaVariant::Fragment, SchemaVariant::Mutable)]
public string $internalNotes;
}
// Fragment schema won't
#[ExcludeFromVariant(SchemaVariant::Fragment)]
#[ExcludeFromVariant(SchemaVariant::Mutable)]
public string $internalNotes;
use Garden\Schema\Entity;
use Garden\Schema\IncludeOnlyInVariant;
use Garden\Schema\SchemaVariant;
class User extends Entity {
public int $id;
public string $username;
public string $email;
// Only lic ?string $inviteCode;
}
// Create schema iant::Fragment);
use Garden\Schema\Entity;
use Garden\Schema\SchemaVariant;
// Invalidate a specific variant for a class
Entity::invalidateSchemaCache(Article::class, SchemaVariant::Full);
// Invalidate all variants for a class
Entity::invalidateSchemaCache(Article::class);
// Invalidate all cached schemas globally
Entity::invalidateSchemaCache();
use Garden\Schema\EntitySchemaCache;
use Garden\Schema\Schema;
use Garden\Schema\SchemaVariant;
// Check if a schema is cached
if (EntitySchemaCache::has(Article::class, SchemaVariant::Full)) {
$schema = EntitySchemaCache::get(Article::class, SchemaVariant::Full);
}
// Get or create a schema with a factory function
$schema = EntitySchemaCache::getOrCreate(
MyEntity::class,
SchemaVariant::Full,
function () {
// Build and return the schema
return new Schema([
'type' => 'object',
'properties' => [...],
]);
}
);
// Invalidate specific cache entries
EntitySchemaCache::invalidate(Article::class, SchemaVariant::Full);
EntitySchemaCache::invalidate(Article::class); // All variants for class
EntitySchemaCache::invalidateAll(); // Everything
// Debugging helpers
$count = EntitySchemaCache::count();
$all = EntitySchemaCache::getAll();
// Define a custom variant enum
enum AccessLevel: string {
case Public = 'public';
case Admin = 'admin';
case Internal = 'internal';
}
use Garden\Schema\Entity;
use Garden\Schema\ExcludeFromVariant;
use Garden\Schema\IncludeOnlyInVariant;
class User extends Entity {
public int $id;
public string $name;
// Only visible to admins and internal
#[ExcludeFromVariant(AccessLevel::Public)]
public string $adminNotes = '';
// Only visible internally
#[IncludeOnlyInVariant(AccessLevel::Internal)]
public string $internalSecret = '';
}
// Use custom variants with getSchema()
$publicSchema = User::getSchema(AccessLevel::Public);
$adminSchema = User::getSchema(AccessLevel::Admin);
$internalSchema = User::getSchema(AccessLevel::Internal);
$user = User::from([...]);
$user->adminNotes = 'Secret note';
// Serialize with a specific variant - only alization variant for the entity
$user->setSerializationVariant(AccessLevel::Public);
// Now toArray() and json_encode() will use the set variant
$array = $user->toArray(); // Uses AccessLevel::Public
$json = json_encode($user); // Uses AccessLevel::Public
// Get the current serialization variant
$variant = $user->getSerializationVariant(); // AccessLevel::Public
// Clear the serialization variant (reverts to full schema)
$user->setSerializationVariant(null);
// Explicit variant in toArray() overrides the set serialization variant
$adminArray = $user->toArray(AccessLevel::Admin); // Uses Admin regardless of set variant
class Article extends Entity {
public int $id;
public User $author;
#[ExcludeFromVariant(AccessLevel::Public)]
public string $editorNotes = '';
}
$article = Article::from([...]);
$article->author->adminNotes = 'Author admin note';
$article->editorNotes = 'Editor note';
// Both Article and nested User will use Public variant
$publicArray = $article->toArray(AccessLevel::Public);
// $publicArray won't
use Garden\Schema\Entity;
use Garden\Schema\ExcludeFromVariant;
use Garden\Schema\NestedVariant;
use Garden\Schema\SchemaVariant;
class Author extends Entity {
public int $id;
public string $name;
public string $email;
#[ExcludeFromVariant(SchemaVariant::Fragment)]
public string $bio = ''; // Only in Full variant
}
class Article extends Entity {
public int $id;
public string $title;
#[ExcludeFromVariant(SchemaVariant::Fragment)]
public string $body = '';
// Author is always serialized as Fragment, even when Article is Full
#[NestedVariant(SchemaVariant::Fragment)]
public ?Author $author = null;
}
$article = Article::from([
'id' => 1,
'title' => 'Hello World',
'body' => 'Full content...',
'author' => [
'id' => 10,
'name' => 'John Doe',
'email' => '[email protected]',
'bio' => 'Author biography...',
],
]);
// Serialize article as Full
$array = $article->toArray(SchemaVariant::Full);
// Article has body (Full), but author does NOT have bio (Fragment)
// $array = [
// 'id' => 1,
// 'title' => 'Hello World',
// 'body' => 'Full content...',
// 'author' => [
// 'id' => 10,
// 'name' => 'John Doe',
// 'email' => '[email protected]',
// // bio is excluded - author uses Fragment variant
// ],
// ]
$schema = Article::getSchema(SchemaVariant::Full);
// The 'author' property in the schema will use Fragment schema (no bio property)
class Article extends Entity {
public int $id;
#[NestedVariant(SchemaVariant::Fragment)]
#[PropertySchema(['items' => ['entityClassName' => Author::class]])]
public array $contributors = [];
}
// All contributors will be serialized as Fragment
$array = $article->toArray(SchemaVariant::Full);
class Address extends Entity {
public string $street;
public string $city;
}
class Person extends Entity {
public string $name;
public Address $address;
public ?Address $workAddress = null;
}
$person = Person::from([
'name' => 'Jane',
'address' => ['street' => '123 Main St', 'city' => 'Springfield'],
]);
$person->address; // Address instance
$person->address->city; // 'Springfield'
use Garden\Schema\Entity;
use Garden\Schema\EntityDefaultInterface;
class Metadata extends Entity implements EntityDefaultInterface {
public string $version;
public bool $draft;
public static function default(): static {
$instance = new static();
$instance->version = '1.0';
$instance->draft = true;
return $instance;
}
}
class Article extends Entity {
public string $title;
public Metadata $metadata; // Gets default when not provided
public ?Metadata $extraMeta; // Also gets default when not provided
}
use Garden\Schema\EntityInterface;
use Garden\Schema\EntityTrait;
use Garden\Schema\Schema;
use Garden\Schema\SchemaVariant;
class MyModel extends SomeFrameworkModel implements EntityInterface {
use EntityTrait;
private static ?Schema $schema = null;
public int $id;
public string $name;
public static function getSchema(?\BackedEnum $variant = null): Schema {
// Implement your own schema generation or caching
if (self::$schema === null) {
self::$schema = new Schema([
'type' => 'object',
'properties' => [
'id' => ['type' => 'integer'],
'name' => ['type' => 'string'],
],
'
// Type hints work with the interface
function processEntity(EntityInterface $entity): array {
return $entity->toArray();
}
// Works with both Entity subclasses and EntityInterface implementations
$result = processEntity(new MyModel());
$result = processEntity(User::from([...]));
[
// You can specify nullable as a property attribute.
'opt1:s?' => ['nullable' => true],
// You can specify null as an optional type in the declaration.
'opt2:s|n?' => 'Another nullable, optional property.'
]
$schema = Schema::parse(['id:i', 'name:s']);
try {
// $u1 will be ['id' => 123, 'name' => 'John']
$u1 = $schema->validate(['id' => '123', 'name' => 'John']);
// This will thow an exception.
$u2 = $schema->validate(['id' => 'foo']);
} catch (ValidationException $ex) {
// $ex->getMessage() will be: 'id is not a valid integer. name is
$schema = Schema::parse(['page:i', 'count:i?']);
if ($schema->isValid(['page' => 5]) {
// This will be hit.
}
if ($schema->isValid(['page' => 2, 'count' => 'many']) {
// This will not be hit because the data isn't valid.
}
$sch = new Schema($userArray);
$sch->setRefLookup(new ArrayRefLookup($components));
$valid = $sch->validate(...);
use Garden\Schema\Schema;
$schema = Schema::parse([]);
// Enable a flag.
$schema->setFlag(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE, true);
// Disable a flag.
$schema->setFlag(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE, false);
// Set all flags together.
$schema->setFlags(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE & Schema::VALIDATE_EXTRA_PROPERTY_NOTICE);
// Check if a flag is set.
$schema->hasFlag(Schema::VALIDATE_STRING_LENGTH_AS_UNICODE); // true
function (mixed $value, ValidationField $field): bool {
}
function (mixed $value, ValidationField $field): mixed {
}
$schema = new Schema([...]);
// By default schema returns instances of DateTimeImmutable, instead return a string.
$schema->addFormatFilter('date-time', function ($v) {
$dt = new \DateTime($v);
return $dt->format(\DateTime::RFC3339);
}, true);
class LocalizedValidation extends Validation {
public function translate($str) {
if (substr($str, 0, 1) === '@') {
// This is a literal string that bypasses translation.
return substr($str, 1);
} else {
return gettext($str);
}
}
}
// Install your class like so:
$schema = Schema::parse([...]);
$schema->setValidationClass(LocalizedValidation::class);
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.