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 Implementing the Tweet\Type class

Implementing the Tweet\Type class

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.

Identification method: getFieldTypeIdentifier()

It must return the string that unique= ly identifies this FieldType (DataTypeString in eZ Publish 4). We will use = "eztweet":

eZ/FieldType/Tweet/Type
=20
public function getFieldTypeIdentifier()
{
   return 'eztweet';
}
=20

Value handling methods: creat= eValueFromInput() and checkValueStructure()

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). This should do:

 

=20
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, we want to be sure that Tweet<= /span>\Value::$url is a string:

 

=20
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() will always be used, even if the FieldType is directly fed a Value object, and not a string.

Va= lue initialization: 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.

 

=20
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 fromHash() or toHash(= ) methods.

Val= idation 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:

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:

 

=20
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:

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:

 

=20
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:

 

=20
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:=

 

=20
   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.

Metadata handlin= g methods: getName() and getSortInfo().

FieldTypes require two methods relate= d to Field metadata:

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:

 

=20
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(), as it allows us to sort on the tweet=E2=80= =99s author and on the tweet=E2=80=99s ID.

FieldType seria= lization methods: 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:

 

=20
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

Persistence methods: fromPersistence= Value and toPersistenceValue

Storage of fieldType data is done through the persistence layer (SPI).

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:

The 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():=

Tweet\Type
=20
/**
 * @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

 

Fet= ching data from the twitter API
As explained in the tutorial's introduct= ion, we will enrich our tweet's URL with the embed version, fetched using t= he twitter API. To do so, we will, when toPersistenceValue()&n= bsp;is called, fill in the value's contents property from this method, befo= re creating the PersistenceValue object.
First, we need a twitter client in = Tweet\Type. For convenience, we provide one in this tutorial's= bundle:
  • The Twitter\TwitterClient class:
  • The Twitter\TwitterClientInterface interface
  • An ezsystems.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.

Injecting the twitter client into 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:

eZ/Publish/FieldType/Tweet/Type.php:
=20
use EzSystems\TweetFieldTypeBundle\Twitter\TwitterClientInterface;
 
class Type extends FieldType
{
    /** @var TwitterClientInterface */
    protected $twitterClient;

    public function __construct( TwitterClientInterface $twitterClient )
    {
        $this->twitterClient =3D $twitterClient;
    }
}
=20
Completing the value using the twitter client

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:

eZ/Publish/FieldType/Tweet/Type.php
=20
 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.

------=_Part_3517_303801500.1485853478595--