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

Implement the Tweet\Type class

=20
=20
=20
=20

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.

Identification method: getFieldTypeIdentifier()

It must return the string that uniquely identifies this Field Type (Data= TypeString in legacy). We will use "eztweet":

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

Value handling methods: 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:

  • either return the Value object it was able to create out of the input v= alue,
  • or return this value untouched. The API will detect this and inform tha= t the input value was not accepted.

The only acceptable value for our type is the URL of a tweet (we could o= f 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 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:

 

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

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

 

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

Validation methods: 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:

 

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

 

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

 

=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 o= n 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 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 TweetAuthorValidator key, we check that the username in the stat= us url matches one of the valid authors.

Metadata handling methods: 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:

 

=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() 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.

Field Type serialization methods: 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.  

 

=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: 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 features
  • externalData =E2=80=93 external data, stored using a custo= m storage handler
  • sortKey =E2=80=93 sort value used for sorting

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

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

 

Fetchi= ng data from the Twitter API

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:

  • 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 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>

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

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.

 

=20
=20
=20
=20
Tutorial path
=20

=20
    =20
    =20
    =20
=20
=20 =20 = =20 =20 =20 =20 =20 =20 =20 = =20 =20 =20 =20 =20
=20 =20
=20
=20
=20

=20
=20
=20
=20
------=_Part_3773_1178572581.1485855284550--