Applying Annotations
Annotations can be applied programmatically when building the schema, or via PHP attributes that the AttributeReader picks up.
Programmatic annotations
Pass annotations to any Edm constructor via the annotations parameter:
use LaravelUi5\OData\Edm\Annotation\Annotation;
use LaravelUi5\OData\Edm\Annotation\ConstantAnnotationValue;
use LaravelUi5\OData\Edm\Property\Property;
use LaravelUi5\OData\Edm\Type\EntityType;
use LaravelUi5\OData\Edm\Type\PrimitiveType;
use LaravelUi5\OData\Edm\Contracts\Container\PrimitiveTypeEnum;
// Annotate a property
$nameProp = new Property(
name: 'name',
type: new PrimitiveType(PrimitiveTypeEnum::String),
annotations: [
new Annotation(
term: 'Org.OData.Core.V1.Description',
value: new ConstantAnnotationValue('The product name'),
),
],
);
// Annotate an entity type
$productType = new EntityType(
namespace: 'App.Products',
name: 'Product',
key: [$idProp],
declaredProperties: [$idProp, $nameProp],
annotations: [
new Annotation(
term: 'Org.OData.Core.V1.Description',
value: new ConstantAnnotationValue('A product in the catalog'),
),
],
);Annotation value types
Constant value -- a simple string, number, or boolean:
new ConstantAnnotationValue('Hello World')Record value -- a named set of property values:
use LaravelUi5\OData\Edm\Annotation\RecordAnnotationValue;
new RecordAnnotationValue(
properties: [
'TypeName' => new ConstantAnnotationValue('App.Products.Product'),
'Title' => new RecordAnnotationValue(
properties: [
'Value' => new ConstantAnnotationValue('name'),
],
),
],
typeName: 'com.sap.vocabularies.UI.v1.HeaderInfoType',
)Collection value -- an ordered list:
use LaravelUi5\OData\Edm\Annotation\CollectionAnnotationValue;
new CollectionAnnotationValue([
new RecordAnnotationValue(properties: [
'Value' => new ConstantAnnotationValue('name'),
'Label' => new ConstantAnnotationValue('Product Name'),
]),
new RecordAnnotationValue(properties: [
'Value' => new ConstantAnnotationValue('price'),
'Label' => new ConstantAnnotationValue('Price'),
]),
])Attribute-based annotations
Vocabulary term classes can be used as PHP attributes. When models are registered via discoverModel(), ModelDiscovery automatically reads class-level and property-level vocabulary attributes and attaches them to the EntityType and Property objects. They then appear in the $metadata CSDL XML.
Class-level annotations
Class-level annotations (applied to the model class) work directly:
use LaravelUi5\OData\Vocabularies\Core\V1\Description;
use LaravelUi5\OData\Vocabularies\Ui\V1\LineItem;
use LaravelUi5\OData\Vocabularies\Ui\V1\SelectionFields;
#[Description('A product in the catalog')]
#[SelectionFields(['name', 'category'])]
#[LineItem(['name', 'category', 'price'])]
class Product extends Model
{
// ...
}These are placed on the <EntityType> element in $metadata.
Property-level annotations and PHP 8.4 property hooks
Property-level annotations require a declared PHP property to attach to. However, Eloquent models store their data in an internal $attributes array accessed via __get() magic -- there are no PHP class properties for database columns by default.
Declaring a bare typed property breaks Eloquent. If you write public string $name;, PHP accesses the declared (uninitialized) property directly instead of falling through to __get(), causing a TypeError.
The solution is PHP 8.4 property hooks. They give you a real PHP property (visible to reflection, can carry attributes) while delegating access to Eloquent's attribute system:
use LaravelUi5\OData\Vocabularies\Common\V1\Label;
use LaravelUi5\OData\Vocabularies\Core\V1\Description;
use LaravelUi5\OData\Vocabularies\Ui\V1\Hidden;
#[Description('A product in the catalog')]
#[SelectionFields(['name', 'category'])]
class Product extends Model
{
#[Label('Product Name')]
#[Description('The display name of the product')]
public string $name {
get => $this->getAttribute('name');
set(string $value) => $this->setAttribute('name', $value);
}
#[Label('Category')]
public string $category {
get => $this->getAttribute('category');
set(string $value) => $this->setAttribute('category', $value);
}
#[Hidden]
public ?int $internal_flags {
get => $this->getAttribute('internal_flags');
set(?int $value) => $this->setAttribute('internal_flags', $value);
}
}This pattern:
- Works with Eloquent:
getAttribute()/setAttribute()preserves casts, mutators, and dirty tracking - Supports reflection:
AttributeReaderreads the vocabulary attributes from the declared property - Only needed for annotated properties: columns without annotations continue through
__get()as usual
How discovery wires it
No extra registration needed. When you call discoverModel(), ModelDiscovery automatically:
- Reads class-level vocabulary attributes via
AttributeReader::readClass() - Reads property-level vocabulary attributes via
AttributeReader::readProperty()for each column that has a declared PHP property on the model class - Passes the annotations to
EntityTypeandPropertyconstructors - The
CsdlSerializerserializes them into the$metadataXML
protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface
{
$this->discoverModel(Product::class);
return $builder
->namespace('App.Products')
->useVocabulary(Vocabulary::Core)
->useVocabulary(Vocabulary::UI)
->useVocabulary(Vocabulary::Common);
}Reading attributes manually
For advanced use cases outside of model discovery, use AttributeReader directly:
use LaravelUi5\OData\Service\Discovery\AttributeReader;
$reader = new AttributeReader();
// Class-level annotations
$annotations = $reader->readClass(new ReflectionClass(Product::class));
// Property-level annotations
$annotations = $reader->readProperty(
new ReflectionProperty(Product::class, 'name')
);
// Parameter-level annotations (for function parameters)
$annotations = $reader->readParameter($reflectionParameter);The AttributeReader only picks up attributes that implement TypedAnnotationInterface. Other PHP attributes (routing, validation, ORM) are silently ignored.
Vocabulary references
When annotations use terms from external vocabularies (Core, UI, Common, etc.), the $metadata document must include the corresponding <edmx:Reference> elements so that clients can resolve the term definitions.
For any of the 11 built-in vocabularies, use useVocabulary() with the Vocabulary enum:
use LaravelUi5\OData\Edm\Vocabularies\Vocabulary;
protected function configure(EdmBuilderInterface $builder): EdmBuilderInterface
{
$builder
->useVocabulary(Vocabulary::Core)
->useVocabulary(Vocabulary::UI);
// ... entity types, sets, etc.
return $builder->namespace($this->namespace());
}Available enum cases: Core, Validation, Measures, Aggregation, Authorization, Capabilities, Common, UI, Analytics, Communication, PersonalData.
For vocabularies outside the built-in set, use addReference() directly:
use LaravelUi5\OData\Edm\IncludedSchema;
use LaravelUi5\OData\Edm\Reference;
$builder->addReference(new Reference(
uri: 'https://example.com/vocabularies/Custom.xml',
includes: [new IncludedSchema(namespace: 'com.example.Custom.v1', alias: 'Custom')],
));Each vocabulary whose terms you use needs its own reference. Without it the annotations will still appear in the XML, but clients may not be able to interpret them.
Where annotations appear
Annotations are serialized into the $metadata CSDL XML document:
<EntityType Name="Product">
<Annotation Term="Org.OData.Core.V1.Description" String="A product in the catalog"/>
<Property Name="name" Type="Edm.String">
<Annotation Term="com.sap.vocabularies.Common.v1.Label" String="Product Name"/>
</Property>
</EntityType>UI5 clients read these annotations to auto-configure SmartTable columns, form fields, filter bars, and value help dialogs.