Skip to content
Merged
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Attributes are in `src/Attribute/`:
- `#[Entity]` - Marks an entity for GraphQL exposure
- `#[Field]` - Exposes a field
- `#[Association]` - Exposes an association (relationship)
- `#[ComputedField]` - Exposes derived values from entity methods (placed on public methods)
- `#[ExcludeFilters]` - Excludes specific filters

### Event System (PSR-14)
Expand Down
46 changes: 45 additions & 1 deletion docs/attributes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Attributes
==========

Configuration of your entities for GraphQL is done with PHP attributes.
There are three attributes and all options for each are covered in this
There are four attributes and all options for each are covered in this
document.

The namespace for attributes is ``ApiSkeletons\Doctrine\ORM\GraphQL\Attribute``.
Expand Down Expand Up @@ -137,6 +137,50 @@ associated with. Associations of the to many variety will become connections.
* ``hydratorStrategy`` - A custom hydrator strategy class.
Class must be injected into the HydratorFactory container. See `containers <containers.html>`_


ComputedField
=============

Used on public methods to expose computed values derived from entity logic.
Computed fields are values calculated from other properties or business logic,
not stored directly in the database.

* ``type`` - **Required**. The GraphQL type name (e.g., ``'string'``, ``'int'``).
Must match a registered type in the TypeContainer.
* ``description`` - A description of the computed field.
* ``name`` - An override for the field name in GraphQL. If not provided,
the name is derived from the method name (``getFullName`` becomes ``fullName``).
* ``group`` - You can have multiple GraphQL configurations organized by ``group``.

Example:

.. code-block:: php

#[GraphQL\Entity]
class Artist
{
#[GraphQL\Field]
private string $firstName;

#[GraphQL\Field]
private string $lastName;

#[GraphQL\ComputedField(
type: 'string',
description: 'Full name of the artist'
)]
public function getFullName(): string
{
return $this->firstName . ' ' . $this->lastName;
}
}

**Important**: Computed fields cannot be filtered at the database level
and will not appear in filter InputObjects. They are calculated in PHP
after data is retrieved from the database.

For more information, see `computed-fields <computed-fields.html>`_.

.. role:: raw-html(raw)
:format: html

Expand Down
164 changes: 155 additions & 9 deletions docs/computed-fields.rst
Original file line number Diff line number Diff line change
@@ -1,15 +1,160 @@
===============
Computed Fields
===============

You may add any computed field to an entity definition. This is done with the
`EntityDefinition Event <events.html>`_.
Computed fields allow you to expose derived values from entity methods in your GraphQL schema
without storing them in the database. Common use cases include full names, formatted values,
calculations, and other business logic.

Using the ComputedField Attribute
==================================

The simplest way to add a computed field is with the ``#[ComputedField]`` attribute.
This attribute is placed on a public method in your entity.

Basic Example
-------------

.. code-block:: php

use ApiSkeletons\Doctrine\ORM\GraphQL\Attribute as GraphQL;

#[GraphQL\Entity]
class Artist
{
#[GraphQL\Field]
private string $firstName;

#[GraphQL\Field]
private string $lastName;

#[GraphQL\ComputedField(
type: 'string',
description: 'Full name of the artist'
)]
public function getFullName(): string
{
return $this->firstName . ' ' . $this->lastName;
}
}

This creates a ``fullName`` field in your GraphQL schema that calls the ``getFullName()``
method when queried. The field name is automatically derived from the method name
by removing the ``get`` prefix.

.. code-block:: graphql

query {
artists {
edges {
node {
firstName
lastName
fullName
}
}
}
}

ComputedField Parameters
------------------------

The ``#[ComputedField]`` attribute accepts these parameters:

* ``type`` - **Required**. The GraphQL type name (e.g., ``'string'``, ``'int'``, ``'boolean'``).
Must match a registered type in the TypeContainer.
* ``description`` - Optional. A description of the computed field for GraphQL schema documentation.
* ``name`` - Optional. Override the field name in the GraphQL schema. If not provided,
the name is derived from the method name.
* ``group`` - Optional. The attribute group (default: ``'default'``).

Custom Field Names
------------------

By default, field names are derived from method names:

* ``getFullName()`` becomes ``fullName``
* ``isActive()`` stays ``isActive`` (for boolean methods)
* Other methods use the method name as-is

You can override this with the ``name`` parameter:

.. code-block:: php

#[GraphQL\ComputedField(
type: 'string',
name: 'displayName',
description: 'Display name for UI'
)]
public function getFullDisplayName(): string
{
return 'Artist: ' . $this->name;
}

This creates a ``displayName`` field instead of ``fullDisplayName``.

Multiple Computed Fields
-------------------------

You can add multiple computed fields to the same entity:

.. code-block:: php

#[GraphQL\Entity]
class User
{
#[GraphQL\Field]
private string $name;

#[GraphQL\Field]
private string $email;

#[GraphQL\ComputedField(type: 'string', description: 'Full name')]
public function getFullName(): string
{
return $this->name . ' (' . $this->email . ')';
}

#[GraphQL\ComputedField(type: 'string', description: 'Email domain')]
public function getEmailDomain(): string
{
return substr($this->email, strpos($this->email, '@') + 1);
}

#[GraphQL\ComputedField(type: 'boolean', description: 'Is user active')]
public function isActive(): bool
{
return $this->password !== '';
}
}

How Computed Fields Work
-------------------------

Computed fields are:

* **Integrated with the hydrator** - Values are extracted along with regular fields
* **Cached per request** - If you enable ``useHydratorCache``, computed values are cached
* **Not filterable** - Computed fields cannot be used in database filters since they're
calculated in PHP, not at the database level
* **Lazy evaluated** - Only computed when explicitly requested in a GraphQL query

Filtering Limitations
---------------------

Computed fields do not appear in filter InputObjects because they cannot be filtered
at the database level. If you need to filter on computed values, consider storing
them in the database or using the `QueryBuilder Event <events.html>`_ to add custom filters.


Advanced: Event-Based Computed Fields
======================================

Modify an Entity Definition
---------------------------
For more complex scenarios, such as computed fields that require database queries
or external service calls, you can use the `EntityDefinition Event <events.html>`_.

You may modify the array used to define an entity type before it is created.
This can be used for computed data. You must attach a listener
before defining your GraphQL schema.
This approach gives you full control over the field definition and resolver logic.
You must attach a listener before defining your GraphQL schema.

Events of this type are named ``Entity::class . '.definition'`` and the event
name cannot be modified.
Expand All @@ -22,6 +167,7 @@ name cannot be modified.
use App\ORM\Entity\Performance;
use Doctrine\ORM\EntityManager;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use League\Event\EventDispatcher;

$driver = new Driver($entityManager);
Expand All @@ -47,10 +193,10 @@ name cannot be modified.
$queryBuilder
->select('COUNT(performance)')
->from(Performance::class, 'performance')
->andWhere($queryBuilder->expr('performance.artist', ':artistId'))
->andWhere('performance.artist = :artistId')
->setParameter('artistId', $objectValue->getId());

return $queryBuilder->getQuery()->getScalarResult();
return (int) $queryBuilder->getQuery()->getSingleScalarResult();
},
];

Expand Down
2 changes: 1 addition & 1 deletion docs/just-the-basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ all optional.
The first step is to add attributes to your entities.
Attributes are stored in the namespace
``ApiSkeletons\Doctrine\ORM\GraphQL\Attribute`` and there are attributes for
``Entity``, ``Field``, and ``Association``. Use the appropriate attribute on
``Entity``, ``Field``, ``Association``, and ``ComputedField``. Use the appropriate attribute on
each element you want to be queryable from GraphQL.

.. code-block:: php
Expand Down
2 changes: 1 addition & 1 deletion docs/technical/COMPLETE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ API Reference (2,918 lines)
Detailed documentation of all 10 configuration options with use cases and examples.

**attributes-reference.rst** (1,182 lines)
Comprehensive reference for all PHP attributes (#[Entity], #[Field], #[Association]).
Comprehensive reference for all PHP attributes (#[Entity], #[Field], #[Association], #[ComputedField]).

**filters-reference.rst** (1,167 lines)
Complete filter system documentation covering all 15 filter types.
Expand Down
Loading
Loading