Message-ID: <367505573.3774.1485855284550.JavaMail.confluence@ip-10-127-227-164> Subject: Exported From Confluence MIME-Version: 1.0 Content-Type: multipart/related; boundary="----=_Part_3773_1178572581.1485855284550" ------=_Part_3773_1178572581.1485855284550 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: quoted-printable Content-Location: file:///C:/exported.html
As said in the introduction, the Type class of a Field Type must impleme=
nt eZ\Publish\SPI\FieldType\FieldType
(later referred to as "F=
ield Type interface").
All native Field Types also extend the eZ\Publish\Core\FieldType\F=
ieldType
abstract class that implements this interface and provides =
implementation facilities through a set of abstract methods of its own. In =
this case, Type classes implement a mix of methods from the Field Type inte=
rface and from the abstract Field Type.
Let=E2=80=99s go over those methods and their implementation.
getFieldTypeIdentifier()
It must return the string that uniquely identifies this Field Type (Data=
TypeString in legacy). We will use "eztweet
":
public function getFieldTypeIdentifier() { return 'eztweet'; }=20
createVa=
lueFromInput()
and checkValueStructure()
Both methods are used by the abstract Field Type implementation of acceptValue()
. This Field Type interface method checks and tra=
nsforms various input values into the type's own Value class: eZ\Fiel=
dType\Tweet\Value
. This method must:
The only acceptable value for our type is the URL of a tweet (we could o= f course imagine more possibilities). This should do:
protected function createValueFromInput( $inputValue ) { if ( is_string( $inputValue ) ) { $inputValue =3D new Value( array(= 'url' =3D> $inputValue ) ); } return $inputValue; }=20
Use this method to provide convenient ways to set an attribute=E2=
=80=99s value using the API. This can be anything from primitives to comple=
x business objects.
Next, we will implement checkValueStructure()
. It is calle=
d by the abstract Field Type to ensure that the Value fed to the Type is ac=
ceptable. In our case, we want to be sure that Tweet
\V=
alue::$url
is a string:
protected function checkValueStructure( BaseValue $value ) { if ( !is_string( $value->url ) ) { throw new eZ\Publish\Core\Base\Ex= ceptions\InvalidArgumentType( '$value-&= gt;url', 'string', $value-&g= t;url ); } }=20
Yes, we execute the same check as in createValueFromInput()
=
. But both methods aren't responsible for the same thing. The first will, <=
em>if given something else than a Value of its type, try to convert it=
to one. checkValueStructure()
will always be used, even =
if the Field Type is directly fed a Value
object, and not a st=
ring.
getEmptyValue()
This method provides what is considered as an empty value of this type, = depending on our business requirements. No extra initialization is required= in our case.
public function getEmptyValue() { return new Value; }=20
If you run the unit tests at this point, you should get about five failu=
res, all of them on the fromHash()
or toHash()
m=
ethods.
validateValidatorConf=
iguration()
and validate()
The Type class is also responsible for validating input data (to a Field
), as well as configuration input data (to a FieldDefini=
tion
). In this tutorial, we will run two validation operations on in=
put data:
validate submitted urls, ensuring they actually reference a twitter = status;
limit input to a known list of authors, as an optional validation st= ep.
validateValidatorConfiguration()
will be called when an in=
stance of the Field Type is added to a Content Type, to ensure that the val=
idator configuration is valid. For a TextLine (length validation), it means=
checking that both min length and max length are positive integers, and th=
at min is lower than max.
When an instance of the type is added to a Content Type, validateV=
alidatorConfiguration()
receives the configuration for the vali=
dators used by the Type as an array. It must return an array of error messa=
ges if errors are found in the configuration, and an empty array if no erro=
rs were found.
For TextLine, the provided array looks like this:
array( 'StringLengthValidator' =3D> array( 'minStringLength' =3D> 0, 'maxStringLength' =3D> 100 ) );=20
The structure of this array is totally free, and up to each type impleme= ntation. We will in this tutorial mimic what is done in native Field Types:=
Each level one key is the name of a validator, as acknowledged by the Ty= pe. That key contains a set of parameter name / parameter value rows. We mu= st check that:
all the validators in this array are known to the type
arguments for those validators are valid and have sane values
We do not need to include mandatory validators if they don=E2=80=99t hav= e options. Here is an example of what our Type expects as validation config= uration:
array( =E2=80=98TweetAuthorValidator=E2=80=99 =3D> array( =E2=80=98AuthorList=E2=80=99 =3D&= gt; array( =E2=80=98johndoe=E2=80=99, =E2=80=98janedoe=E2=80=99 ) ) );=20
The configuration says that tweets must be either by johndoe o=
r by janedoe. If we had not provided TweetAuthorValidator at all, it would =
have been ignored.
We will iterate over the items in $validatorConfiguration
a=
nd:
add errors for those we don=E2=80=99t know about;
check that provided arguments are known and valid:
TweetAuthorValidator accepts a non-empty array of valid Twitter user= names
public function validateValidatorConfiguration( $validatorConfigura= tion ) { $validationErrors =3D array(); foreach ( $validatorConfiguration as $validatorIdentifier= =3D> $constraints ) { // Report unknown validators if ( !$validatorIdentifier !=3D '= TweetAuthorValidator' ) { $validati= onErrors[] =3D new ValidationError( "Validator '$validatorIdentifier' is un= known" ); continue; } // Validate arguments from TweetA= uthorValidator if ( !isset( $constraints['Author= List'] ) || !is_array( $constraints['AuthorList'] ) ) { $validati= onErrors[] =3D new ValidationError( "Missing or invalid AuthorList argument= " ); continue; } foreach ( $constraints['AuthorLis= t'] as $authorName ) { if ( !pre= g_match( '/^[a-z0-9_]{1,15}$/i', $authorName ) ) { &nb= sp; $validationErrors[] =3D new ValidationError( "Invalid twitte= r username" ); } } } return $validationErrors; }=20
validate()
is the method that runs the actual validation o=
n data, when a content item is created with a Field of this type:
public function validate( FieldDefinition $fieldD= efinition, SPIValue $fieldValue ) { $errors =3D array(); if ( $this->isEmptyValue( $fie= ldValue ) ) { return $e= rrors; } // Tweet Url validation if ( !preg_match( '#^https?://twi= tter.com/([^/]+)/status/[0-9]+$#', $fieldValue->url, $m ) ) $errors[]= =3D new ValidationError( "Invalid twitter status url %url%", null, array( = $fieldValue->url ) ); $validatorConfiguration =3D $fiel= dDefinition->getValidatorConfiguration(); if ( isset( $validatorConfigurati= on['TweetAuthorValidator'] ) ) { if ( !in_= array( $m[1], $validatorConfiguration['TweetAuthorValidator']['AuthorList']= ) ) { &nb= sp; $errors[] =3D new ValidationError( &nb= sp; "Twitter user %user% is not in the a= pproved author list", &nb= sp; null, &nb= sp; array( $m[1] ) &nb= sp; ); } } return $errors; }=20
First, we validate the url with a regular expression. If it doesn=E2=80=
=99t match, we add an instance of ValidationError
to the ret=
urn array. Note that the tested value isn=E2=80=99t directly embedded in th=
e message but passed as an argument. This ensures that the variable is prop=
erly encoded in order to prevent attacks, and allows for singular/plural ph=
rases using the second parameter.
Then, if our Field Type instance=E2=80=99s configuration contains a
getName()
and =
getSortInfo()
.Field Types require two methods related to Field metadata:
getName()
is used to generate a name out of a Field v=
alue, either to name a Content item (naming pattern in legacy) or to genera=
te a part for an URL Alias.
getSortInfo()
is used by the persistence layer to obt=
ain the value it can use to sort & filter on a Field of this type
Obviously, a tweet=E2=80=99s full URL isn=E2=80=99t really suitable as a=
name. Let=E2=80=99s use a subset of it: <username>-<tweetI=
d>
should be reasonable enough, and suitable for both sorting an=
d naming.
We can assume that this method will not be called if the Field is empty,= and will assume that the URL is a valid twitter URL:
public function getName( SPIValue $value ) { return preg_replace( '#^https?://twitter\.com/([^/]+)/= status/([0-9]+)$#', '$1-$2', (string)$value->url ); } protected function getSortInfo( CoreValue $value ) { return $this->getName( $value ); }=20
In getName()
we run a regular expression replace on the UR=
L to extract the part we=E2=80=99re interested in.
This name is a perfect match for getSortInfo()
as it allow=
s us to sort on the tweet=E2=80=99s author and on the tweet=E2=80=99s ID.=
p>
fromHash()
a=
nd toHash()
Both methods defined in the Field Type interface, are core to the REST A= PI. They are used to export values to serializable hashes.
In our case, it is quite easy:
toHash()
will build a hash with every property from <=
code>Tweet\Value;
fromHash()
will instantiate a Tweet\Value
with the hash it receives.
public function fromHash( $hash ) { if ( $hash =3D=3D=3D null ) { return $this->getEmptyValue(); } return new Value( $hash ); } public function toHash( SPIValue $value ) { if ( $this->isEmptyValue( $value ) ) { return null; } return array( 'url' =3D> $value->url ); }=20
fromPersistenceVal=
ue
and toPersistenceValue
Storage of Field Type data is done through the persistence layer (SPI).<= /p>
Field Types use their own Value objects to expose their contents using t=
heir own domain language. However, to store those objects, the Type needs t=
o map this custom object to a structure understood by the persistence layer=
: PersistenceValue
. This simple value object has three pr=
operties:
data
=E2=80=93 standard data, stored using the storage eng=
ine's native featuresexternalData
=E2=80=93 external data, stored using a custo=
m storage handlersortKey
=E2=80=93 sort value used for sortingThe role of those mapping methods is to convert a Value
&nbs=
p;of the Field Type into a PersistenceValue
and the other way =
around.
About external storage
Whatever is stored in {{externalData}} requires an external storage hand= ler to be written. Read more about external storage on Field Type API and best practi= ces.
External storage is beyond the scope of this tutorial, but many examples= can be found in existing Field Types.
We will follow a simple implementation here: the Tweet\Value
object will be serialized as an array to the code
=
property using fromHash()
and toHash()
:=
/** * @param \EzSystems\TweetFieldTypeBundle\eZ\Publish\FieldType\Tweet\Value = $value * @return \eZ\Publish\SPI\Persistence\Content\FieldValue */ public function toPersistenceValue( SPIValue $value ) { if ( $value =3D=3D=3D null ) { return new PersistenceValue( array( "data" =3D> null, "externalData" =3D> null, "sortKey" =3D> null, ) ); } return new PersistenceValue( array( "data" =3D> $this->toHash( $value ), "sortKey" =3D> $this->getSortInfo( $value ), ) ); } /** * @param \eZ\Publish\SPI\Persistence\Content\FieldValue $fieldValue * @return \EzSystems\TweetFieldTypeBundle\eZ\Publish\FieldType\Tweet\Value */ public function fromPersistenceValue( PersistenceValue $fieldValue ) { if ( $fieldValue->data =3D=3D=3D null ) { return $this->getEmptyValue(); } return new Value( $fieldValue->data ); }=20
As explained in the tutorial's introduction, we will enrich our tweet's =
URL with the embed version, fetched using the Twitter API. To do so, we wil=
l, when toPersistenceValue()
is called, fill in the value=
's contents property from this method, before creating the Persistenc=
eValue
object.
First, we need a twitter client in Tweet\Type
. For con=
venience, we provide one in this tutorial's bundle:
Twitter\TwitterClient
class:Twitter\TwitterClientInterface
interfaceezsystems.tweetbundle.twitter.client
service that =
uses the class above.The interface has one method: getEmbed( $statusUrl )
that, =
given a tweet's URL, returns the embed code as a string. The implementation=
is very simple, for the sake of simplicity, but gets the job done. Ideally=
, it should at the very least handle errors, but it is not necessary here.<=
/p>
Tweet\Type
Our Field Type doesn't have a constructor yet. We will create one, with =
an instance of Twitter\TwitterClientInterface
as the argu=
ment, and store it in a new protected property:
use EzSystems\TweetFieldTypeBundle\Twitter\TwitterClientInterface; class Type extends FieldType { /** @var TwitterClientInterface */ protected $twitterClient; public function __construct( TwitterClientInterface $twitterClient ) { $this->twitterClient =3D $twitterClient; } }=20
As described above, before creating the PersistenceValue
ob=
ject in toPersistenceValue
, we will fetch the tweet's embed co=
ntents using the client, and assign it to Tweet\Value::$data:
public function toPersistenceValue( SPIValue $value ) { // if ( $value =3D=3D=3D null ) // {...} if ( $value->contents =3D=3D=3D null ) { $value->contents =3D $this->twitterClient->getEmbed( $valu= e->url ); } return new PersistenceValue( // array(...) }=20
And that's it! When the persistence layer stores content from our type, = the value will be completed with what the twitter API returns.