Message-ID: <351931986.3518.1485853478595.JavaMail.confluence@ip-10-127-227-164> Subject: Exported From Confluence MIME-Version: 1.0 Content-Type: multipart/related; boundary="----=_Part_3517_303801500.1485853478595" ------=_Part_3517_303801500.1485853478595 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 FieldType must =
implement eZ\Publish\SPI\FieldType\FieldType
(later referred t=
o as "FieldType interface").
All native FieldTypes also extend the eZ\Publish\Core\FieldType\Fi=
eldType
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 FieldType inte=
rface and from the abstract FieldType .
Let=E2=80=99s go over those methods a= nd their implementation.
getFieldTypeIdentifier()
=
h4>
It must return the string that unique=
ly identifies this FieldType (DataTypeString in eZ Publish 4). We will use =
"eztweet
"
public function getFieldTypeIdentifier() { return 'eztweet'; }=20
creat=
eValueFromInput()
and checkValueStructure()
=
h4>
Both methods are used by the abstract=
FieldType implementation of acceptValue()
. This FieldT=
ype 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 typ= e is the URL of a tweet (we could of course imagine more possibilities). = span>This should do:
protected function createValueFromInput( $inputValue ) { if ( is_string( $inputValue ) ) { $inputValue =3D new Value( array(= 'url' =3D> $inputValue ) ); } return $inputValue; }=20
Use thi=
s method to provide convenient ways to set an attribute=E2=80=99s value usi=
ng the API. This can be anything from primitives to complex business object=
s.
Next, we will implement =
checkValueStructure()
. It is called by the abstract FieldType to e=
nsure that the Value fed to the Type is acceptable. In our case, Tweet
<=
/span>\Value::$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 than i=
n createValueFromInput()
. But both methods aren't responsible =
for the same thing. The first will, if given something else than a Valu=
e of its type, try to convert it to one. checkValueStructure()=
code> will always be used, even if the FieldType is directly fed a
object, and not a string.
getEmptyValue()
This method provides what is consider= ed 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 poi=
nt, you should get about five failures, all of them on the
or toHash(=
)
methods.
validateValidatorConfiguration()
and va=
lidate()
The Type class is also responsible fo=
r validating input data (to a Field
), as well as configuration=
input data (to a FieldDefinition
). In this tutorial, we will =
run two validation operations on input data:
validate submitted urls, ensuring they actually reference a tw= itter status;
limit input to a known list of authors, as an optional validat= ion step.
validateValidatorConfiguration(=
)
will be called when an i=
nstance of the FieldType is added to a ContentType, to ensure that the vali=
dator configuration is valid. For a TextLine (length validation), it means =
checking that both min length and max length are positive integers, and tha=
t min is lower than max.
When an instance of the type is added=
to a content type, validateValida=
torConfiguration()
receives the configuration for the va=
lidators used by the Type as an array. I=
t must return an array of error messages if errors are found in the configu=
ration, and an empty array if no errors were found.
&n= bsp;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 totall= y free, and up to each type implementation. We will in this tutorial mimic = what is done in native FieldTypes:
Each level one key is the name of a v= alidator, as acknowledged by the Type. That key contains a set of parameter= name / parameter value rows. We must check that:
all the validators in this array are known to the type<= /p>
arguments for those validators are valid and have sane values<= /span>
We do not need to include mandatory v= alidators if they don=E2=80=99t have options. Here is an example of what our Type expects as validation conf= iguration:
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 con=
figuration says that tweets must be either by johndoe or by janedoe. I=
f we had not provided TweetAuthorValidator at all, it would have been ignor=
ed.
We will iterate over the items in $validatorConfiguration
=
, and:
add errors for those we don=E2=80=99t know about;
= li>check that provided arguments are known and valid:
<= /li>TweetAuthorValidator accepts a non-empty array of valid twitte= r usernames
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 =
on 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 reg=
ular expression. If it doesn=E2=80=99t match, we add an instance of =
ValidationError
to the return array. Note that the tested va=
lue isn=E2=80=99t directly embedded in the message but passed as an argumen=
t. This ensures that the variable is properly encoded in order to prevent a=
ttacks, and allows for singular/plural phrases using the 2nd parameter.
Then, if our FieldType insta=
nce=E2=80=99s configuration contains a TweetAuthorValidator
key, we check that the username in the status url matches one of the=
valid authors.
getName()
and getSortInfo()
.
FieldTypes require two methods relate= d to Field metadata:
getName()
is used to generate a name=
out of a field value, either to name a content item (naming pattern in leg=
acy) or to generate a part for an URL Alias.
getSortInfo()
is used by the persist=
ence layer to obtain the value it can use to sort & filter on a field o=
f 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>-<tweet=
Id>
should be reasonabl=
e enough, and suitable for both sorting and naming.
We can assume that this method will n= ot 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()
, w=
e run a regular expression replace on the URL to extract the part we=E2=80=
=99re interested in.
This name is a perfect match for getSortInfo()
fromHash()
and toHash()
<=
/h4>
Both methods, defined in the FieldTyp= e interface, are core to the REST API. They are used to export values to se= rializable hashes.
In our case, it is quite easy:
toHash()
will build a hash with ever=
y property from 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
fromPersistence=
Value
and toPersistenceValue
Storage of fieldType data is done through the persistence layer (SPI).= p>
FieldTypes use their own Value objects to expose their contents using th=
eir own domain language. However, to store those objects, the Type needs to=
map this custom object to a structure understood by the persistence layer:=
PersistenceValue
. This simple value object has three pro=
perties:
data
standard data, stored using the storage engine's=
native featuresexternalData
external data, stored using a custom sto=
rage handlersortKey
sort value used for sortingThe role of those mapping methods is to convert a Value
&nbs=
p;of the FieldType into a PersistenceValue
, and the other way =
around.
"About external storage"
Whatever is stored in {{externalData}} requires an external storag= e handler 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 ex= amples can be found in existing FieldTypes.
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
toPersistenceValue()
&n=
bsp;is called, fill in the value's contents property from this method, befo=
re creating the PersistenceValue
object.
Tweet\Type
. For convenience, 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 impleme=
ntation 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.
Tweet\Type
Our FieldType doesn't have a constructor yet. We will create one, with a=
n instance of Twitter\TwitterClientInterface
as the argum=
ent, 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.