Repository navigation
Story: Build spatial objects through a reusable fluent creation context #55
Description
Build spatial objects through a reusable fluent creation context
As a developer constructing spatial objects,
I want a reusable creation context and fluent builders for spatial components,
so that I can create complete objects directly or assemble them progressively without repeating their spatial configuration.
Context
Parent Epic: #38.
Depends on the completed #46 public creation contracts and #39–#45 mutable and immutable spatial types and component-ownership contracts.
Public factories provide static, type-specific creation using a shared global mutability mode. This Story introduces a separate builder API with local configuration independent of that global setting.
The only public builder class is LongitudeOne\SpatialTypes\Builder\SpatialBuilder, located at lib/Builder/SpatialBuilder.php. Implementation-only helpers may live under lib/Internal but are not exposed by its API.
The delivery scope covers the currently supported instantiable types, layouts and families exposed by the public factories. Each supported aggregate has distinct direct and progressive creation operations. Estimate this complete scope during refinement and split it if it exceeds the project's Story size limit.
SpatialBuilder::create() returns a new XY Geometry builder. dimension(CoordinateDimensionEnum) returns another SpatialBuilder with the selected layout. Configuration operations return new instances. An aggregate-selection operation such as lineString() returns a new SpatialBuilder with the same configuration and an active aggregate; fluent additions update that returned instance.
One public PHP class cannot expose different native point() or addPoint() parameter names by runtime layout. The approved common signatures are point(float|int|string|null $x = null, float|int|string|null $y = null, float|int|null $z = null, float|int|null $m = null): PointInterface and addPoint(float|int|string|null $x = null, float|int|string|null $y = null, float|int|null $z = null, float|int|null $m = null): SpatialBuilder. XYM callers use the named argument m:. Non-null values for unavailable axes are rejected. This replaces the earlier layout-specific signature proposal.
The public operation matrix is:
Direct creation on SpatialBuilder |
Aggregate selection returning SpatialBuilder |
Progressive addition |
|---|---|---|
point(...) |
— | — |
createLineString(array $points) |
lineString() |
addPoint(...), addPointComponent(PointInterface $point) |
createCircularString(array $points) |
circularString() |
addPoint(...), addPointComponent(PointInterface $point) |
createMultiPoint(array $points) |
multiPoint() |
addPoint(...), addPointComponent(PointInterface $point) |
createCompoundCurve(array $curves = []) |
compoundCurve() |
`addCurve(LineStringInterface |
createPolygon(array $rings) |
polygon() |
addRing(LineStringInterface $ring) |
createTriangle(array $rings) |
triangle() |
addRing(LineStringInterface $ring) |
createMultiLineString(array $lineStrings) |
multiLineString() |
addLineString(LineStringInterface $lineString) |
createMultiPolygon(array $polygons) |
multiPolygon() |
addPolygon(PolygonInterface $polygon) |
createPolyhedralSurface(array $patches = []) |
polyhedralSurface() |
addPatch(PolygonInterface $patch) |
createCollection(array $elements = []) |
collection() |
addElement(SpatialInterface $element) |
The one public class gives point(...) and addPoint(...) the same axis names and native types in every layout. Layout controls which axes can have non-null values and which concrete type is created. No creation operation accepts another SRID argument because the context supplies it. Direct aggregate arrays follow the corresponding constructors, including explicit components where required. polyhedralSurface() and createPolyhedralSurface() reject XY and XYM because those layouts are unsupported. collection() selects Geometry or Geography from the context, and mutable output uses SpatialCollection.
A separate planned Story will relocate existing implementation-only classes to lib/Internal. This builder follows that public/internal boundary from its introduction without performing the repository-wide relocation. Link that Story and clarify sequencing once it exists; no issue number is assumed.
Acceptance Criteria
-
SpatialBuilder::create()provides the public entry point with immutable output, XY coordinates, Geometry family and SRID 0 as its initial configuration. - The context exposes
immutable(),mutable(),dimension(CoordinateDimensionEnum),family(SpatialModelEnum)andsrid(int|SpatialReference)configuration operations. - Each configuration operation returns a new configured context and leaves its source unchanged. Contexts can be reused to create multiple objects.
- Representation is local to the builder. Neither configuring nor using it changes
SpatialFactory's global mode, and changes to that global mode do not change the builder's selected representation. -
SpatialBuilderis the sole public builder class.create(), configuration copies and aggregate-selection operations all returnSpatialBuilder; no layout-specific or aggregate-specific public builder classes are introduced. -
point(...)andaddPoint(...)use the approved common$x,$y,$z,$mparameters, native types and defaults shown above. A non-null value for an unavailable axis is rejected; XYM callers can supplym:by name. -
point(...)directly creates a point in the selected layout, family, reference and representation; each supported aggregate has the listedcreate<Type>(array ...)direct operation and a separate<type>()progressive builder operation. -
addPoint(...)adds coordinates, whereasaddPointComponent(PointInterface)retains a supplied point. Other progressive additions accept only the listed component interfaces, preserving order and the corresponding constructors' accepted member types. -
collection()uses the context family to create GeometryCollectionImmutable or GeographyCollectionImmutable in immutable mode and SpatialCollection in mutable mode. PolyhedralSurface operations reject unsupported XY and XYM layouts. - An aggregate builder with no additions creates the corresponding EMPTY spatial object on
build(); a direct operation with an empty array does likewise where accepted by its constructor. The builder does not infer heterogeneous collection member types from untyped coordinate arrays. - A technically incompatible component is rejected when added, without changing the builder's accumulated state.
build()checks the current state again where mutable shared components may have changed since addition. - Progressive addition operations update and return the same aggregate-building
SpatialBuilderinstance. Additions for a different active aggregate are rejected. - Each successful
build()creates a fresh spatial object, retains accumulated builder state and permits subsequent builds without an implicit reset. Later builder additions do not change membership of objects already built. - Aggregate-building instances retain the context selected when they are obtained. Creating or configuring another builder does not change one already obtained.
- Creation preserves coordinates, null ordinates, EMPTY states and the technical input contracts of the corresponding spatial types. Layout and family are selected through configuration rather than inferred from coordinate values.
- Spatial descendants created from coordinate input use the builder's selected representation, layout, family and declared reference.
- Supplied spatial components retain their identity, metadata and ownership semantics established by Story: Preserve immutable types under explicit names and shared contracts #39–Story: Edit multi-objects and spatial collections in place #45. Repeated builds create distinct aggregate objects without implicitly cloning supplied components.
- Opposite-representation components are rejected without implicit conversion. Shared mutable components remain shared, and edits are observable through their owners under the existing contracts.
- Public builder operations use explicit methods and declared types suitable for IDE completion. Consumers do not depend on magic method dispatch, dynamic method names or internal symbols.
- New implementation-only helpers live under
lib/Internaland do not appear in public method signatures. No other public builder class is required. - Documentation explains direct creation, progressive assembly with one builder class, configuration by copy, aggregate-builder reuse, repeated builds, component sharing and independence from the global factory mode.
Constraints
- Follow supported type/layout/family coverage and constructor-level technical input contracts; do not introduce unsupported spatial types.
- Preserve read, immutable-copy, mutable-editing and ownership contracts established by Epic: Support mutable and immutable spatial objects #38–Story: Create spatial types through public factories with a shared mutability mode #46.
- Configuration selects the context for newly created objects. It does not transform existing coordinates or convert supplied components.
- Geometric validation remains an explicit spatial-validator concern. Do not introduce automatic validation, repair, reprojection or dimension conversion.
- Do not temporarily switch the global factory mode to implement local builder representation.
- Parsing external spatial formats and changes in sibling repositories are outside this Story.
- Moving existing internal classes is separate work and outside this Story.
Definition of Done
The project's common Definition of Done applies.
-
composer test-localpasses. -
composer qualitypasses. - Tests cover initial configuration, each configuration operation, source preservation and reuse of independently configured contexts.
- Tests verify both directions of independence between builder representation and the global factory mode, including failed creation.
- Tests cover the public type/layout/family operation matrix, direct creation and progressive assembly, including EMPTY and partially null coordinate states, plus XYZ/XYM axis distinctions and rejection of unsupported non-null axes.
- Tests verify aggregate-builder identity, retained accumulated state and distinct spatial objects across repeated builds.
- Tests cover supplied-component identity, mutable sharing, nested construction, rejection of opposite representations at addition and build, and preservation of builder state after a rejected addition.
- External-consumer examples use public symbols and demonstrate direct and progressive creation with useful typed completion.
- Public API documentation records supported operations, precise signatures and the public/internal boundary.
Activity