-
-
Notifications
You must be signed in to change notification settings - Fork 4
Binding objects
So far, we have only used associative arrays as a method for storing data. In PHP, associative arrays are the simplest way to store key-value pairs, but using objects instead of associative arrays unlocks more functionality.
In any object-oriented programming language like PHP, objects no not only store data, but also define functions that can be used to manipulate their data or state. In PHP, we can also take advantage of objects' type systems to build dynamic documents that are easy to program and debug.
Imagine a Customer
object that represents a user of an e-commerce website, probably loaded directly from a database:
readonly class Customer {
public function __construct(
public string $id,
public string $name,
public DateTime $createdAt,
) {}
}
It's possible to bind instances of this class to the document without having to make any changes to the object, because by default all public stringable properties can be used as bind keys. The HTML page could look like this:
<h1>Welcome back, <span data-bind:text="name">your name</span>!</h1>
<p>You have been a customer since <time data-bind:text="createdAt">00-00-0000 00:00:00</time>.</p>
Here's some example PHP that constructs a new Customer object and binds it to the document:
function example(DocumentBinder $binder):void {
// The $customer variable could come from a database in a real-world project.
$customer = new Customer(
id: "abc123",
name: "Cody",
dateOfBirth: new DateTime("2016-07-06 15:04:00"),
);
$binder->bindData($customer);
}
Executing the above example function will render the following HTML:
<h1>Welcome back, <span>Cody</span>!</h1>
<p>You have been a customer since <time>2016-07-06 15:04:00</time>.</p>
In the above example, the functionality is no different than using a plain associative array, apart from that there are now strong types being enforced - but imagine if we wanted to add some functionality to display how many years a customer has been active for, rather than outputting the date directly. We can do that by adding a function to the Customer
class that calculates the number of years passed, and then marking that function as being "bindable" by adding the Bind
attribute and providing the bind key years-active
.
The modified HTML page:
<h1>Welcome back, <span data-bind:text="name">your name</span>!</h1>
<p>You have been a customer for <time data-bind:text="years-active">0</time> years.</p>
The modified Customer
class:
readonly class Customer {
public function __construct(
public string $id,
public string $name,
public DateTime $createdAt,
) {}
#[Bind("years-active")]
public function calculateYearsSinceCreation():int {
$now = new DateTime();
$diff = $now->diff($this->createdAt);
return $diff->y;
}
}
The output HTML:
<html><head></head><body><h1>Welcome back, <span>Cody</span>!</h1>
<p>You have been a customer for <time>6</time> years.</p></body></html>
Any public function can be exposed to the DocumentBinder
by adding the Bind
attribute and stating what bind key it represents.
As a convenience helper to the Bind
attribute, using the BindGetter
attribute instead means you do not need to specify the bind key, as it is assumed from the name of the function. It works on any function with a name that begins with get
, and converts the function name to a bind key by trimming the word "get" from the front of the function and converting to camel case.
For example, we can adjust our Customer
class's calculateYearsSinceCreation
function to be named getYearsSinceCreation
, and give it the BindGetter
attribute, to automatically infer the bind key of yearsSinceCreation
.
readonly class Customer {
public function __construct(
public string $id,
public string $name,
public DateTime $createdAt,
) {}
#[BindGetter]
public function getYearsSinceCreation():int {
$now = new DateTime();
$diff = $now->diff($this->createdAt);
return $diff->y;
}
}
Some examples of what a BindGetter function name will convert to in an HTML data bind attribute:
-
getName()
-><span data-bind:text="name">
-
getTotalNum()
-><span data-bind:text="totalNum">
-
getFormattedDate()
-><span data-bind:text="formattedDate">
When binding the document using objects to represent the data, it's possible that objects will have nested properties of other objects. These nested properties can be addressed in the bind key by using the dot character (.
) as a separator.
For example, an object representing a Customer may have int
or string
properties for id
, name
and email
, but other properties like address
may be their own object. Take a look at these three example classes:
class Customer {
public function __construct(
public readonly int $id,
public readonly string $name,
public readonly string $email,
public readonly Address $address,
) {}
}
class Address {
public function __construct(
public readonly string $street,
public readonly string $line2,
public readonly string $city,
public readonly string $postcode,
public readonly Country $country,
) {}
}
class Country {
public function __construct(
public readonly string $code,
) {}
#[BindGetter]
public function getName():string {
return match($this->code) {
"US" => "United States",
"CN" => "China",
"JP" => "Japan",
"DE" => "Germany",
"GB" => "United Kingdom",
"FR" => "France",
"IN" => "India",
"CA" => "Canada",
"IT" => "Italy",
"AU" => "Australia",
"KR" => "South Korea",
default => "Unknown",
};
}
}
With the above classes in your application, an instance of Customer
will reference an instance of Address
as a public property, and the Address
will have an instance of Country
. The Country
class only has a code
as a public property, but a getter function called getName()
, tagged with the BindGetter
Attribute.
Here's an example usage of how HTML bind attributes can be used to access these nested properties, assuming the Customer
object is being bound:
<h1>Customer details (ID <span data-bind:text="id">000</span>)</h1>
<h2 data-bind:text="name">Customer name</h2>
<h3>Address</h3>
<p data-bind:text="address.street">Street address</p>
<p data-bind:text="address.line2">Line 2</p>
<p data-bind:text="address.city">City</p>
<p data-bind:text="address.postcode">Postcode</p>
<p data-bind:text="address.country.name">Country name</p>
The important detail to notice here is the use of nesting using the dot character. In the same bind operation, properties of nested objects can be addressed by separating the objects by their property names. Look at address.country.name
- when binding, the DocumentBinder
will look at the Customer
's address
property, then the Address
's country
property, and then use the Country
's name
property - but the Country
class doesn't have a name
property, and instead it will use the BindGetter
because it matches the name of the function.
With this style of binding, complex data structures can be bound to the page with a single operation, saving lots of repetition and potential debugging time during development.
When working with lists of data, such as an array
of objects, the bindList
function can be used to bind a list of repeating elements to the document.
When using bindData
to bind objects to the document, it's possible that a bound object can also represent a list of data. This can be achieved in one of three ways:
- The object has a public iterable property named according to the
data-bind:list
attribute. - The object implements
Iterator
. - The object implements
IteratorAggregate
.
If an object you are binding is iterable
, and there are data-list
elements in the current bind scope, the object itself will be iterated upon to generate the list of elements. This allows an object to represent an outer record of data as well as a list of associated data. For example, a CalendarDay
object could implement Iterator<CalendarEvent>
, binding the current day's details along with a list of the events within the current day.
Implementing your own Iterator can be done in whatever way you prefer, defining the current
, key
, next
, rewind
, and valid
functions yourself, but the recommended approach due to its simplicity is by implementing IteratorAggregate
.
Applying IteratorAggregate
to a class involves adding a getIterator()
function, which can return any object that is iterable
. By isolating the iterable functionality into a single function, this promotes clean coding of classes and helps visually identify the functionality when scan-reading through code.
Whether you have a Generator
, Traversable
or an array
, you can use the yield from
syntax to return an iterator to your data collection.
A quick example of how an object can be used to define its own set of data, as well as yielding from a collection of other data objects:
<!DOCTYPE html>
<h1>Events for <span data-bind:text="dateString">1st Jan 2000</span>:</h1>
<ol>
<li data-list>
<time data-bind:text="eventStartTimeText">00:00</time>
<p data-bind:text="title">Title of event</p>
</li>
</ol>
function example(Binder $binder, CalendarDay $calendarDay):void {
$binder->bindData($calendarDay);
}
And the two classes that are used to represent the bound data:
class CalendarDay implements IteratorAggregate {
private DateTime $dateTime;
public function __construct(
public int $year,
public int $month,
public int $day,
private EventRepository $eventRepository,
) {
$this->dateTime = new DateTime("$year-$month-$day");
}
#[BindGetter]
public function getDateString():string {
return $this->dateTime->format("jS M Y");
}
/** @return Traversable<CalendarEvent> */
public function getIterator():Traversable {
yield from $this->eventRepository->getEventsForDate(
$this->year,
$this->month,
$this->day,
);
}
}
class CalendarEvent {
public function __construct(
public string $id,
public DateTime $eventStart,
public string $title,
) {}
#[BindGetter]
public function getEventStartTimeText():string {
return $this->eventStart->format("H:i");
}
}
Run this example code locally: https://github.com/PhpGt/DomTemplate/blob/master/examples/binding/14-iterator-aggregate.php
Next, learn about HTML components.
PHP.Gt/DomTemplate is a separately maintained component of PHP.Gt/WebEngine.
- Bind data to HTML elements with
data-bind
attributes - Bind key modifiers
- Inject data into HTML with
{{curly braces}}
- Bind lists of data with
data-list
attributes - Bind nested lists with
data-bind:list
- Automatically remove unbound elements with
data-element
- Bind tabular data into HTML tables
- Using objects to represent bindable data