diff --git a/src/Kore/RatingFieldTypeBundle/DependencyInjection/Configuration.php b/src/Kore/RatingFieldTypeBundle/DependencyInjection/Configuration.php
new file mode 100644
index 0000000000..c1ef4206ed
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/DependencyInjection/Configuration.php
@@ -0,0 +1,29 @@
+root('kore_rating_field_type');
+
+ // Here you should define the parameters that are allowed to
+ // configure your bundle. See the documentation linked above for
+ // more information on that topic.
+
+ return $treeBuilder;
+ }
+}
diff --git a/src/Kore/RatingFieldTypeBundle/DependencyInjection/KoreRatingFieldTypeExtension.php b/src/Kore/RatingFieldTypeBundle/DependencyInjection/KoreRatingFieldTypeExtension.php
new file mode 100644
index 0000000000..a6c7be9f9c
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/DependencyInjection/KoreRatingFieldTypeExtension.php
@@ -0,0 +1,36 @@
+prependExtensionConfig('ezpublish', $config);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function load(array $configs, ContainerBuilder $container)
+ {
+ $configuration = new Configuration();
+ $config = $this->processConfiguration($configuration, $configs);
+
+ $loader = new Loader\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
+ $loader->load('services.xml');
+ }
+}
diff --git a/src/Kore/RatingFieldTypeBundle/KoreRatingFieldTypeBundle.php b/src/Kore/RatingFieldTypeBundle/KoreRatingFieldTypeBundle.php
new file mode 100644
index 0000000000..2ae8c1a8b3
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/KoreRatingFieldTypeBundle.php
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Kore/RatingFieldTypeBundle/Resources/views/field_edit.html.twig b/src/Kore/RatingFieldTypeBundle/Resources/views/field_edit.html.twig
new file mode 100644
index 0000000000..2b8ddc6dbc
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Resources/views/field_edit.html.twig
@@ -0,0 +1 @@
+Edit?
diff --git a/src/Kore/RatingFieldTypeBundle/Resources/views/field_view.html.twig b/src/Kore/RatingFieldTypeBundle/Resources/views/field_view.html.twig
new file mode 100644
index 0000000000..84c5443671
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Resources/views/field_view.html.twig
@@ -0,0 +1,6 @@
+{% block koreRating_field %}
+ {% set field_value %}
+ {{ field.value.rating }}
+ {% endset %}
+ {{ block( 'simple_block_field' ) }}
+{% endblock %}
diff --git a/src/Kore/RatingFieldTypeBundle/Resources/views/fielddefinition_settings.html.twig b/src/Kore/RatingFieldTypeBundle/Resources/views/fielddefinition_settings.html.twig
new file mode 100644
index 0000000000..0ed0944762
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Resources/views/fielddefinition_settings.html.twig
@@ -0,0 +1,3 @@
+{% block koreRating_settings %}
+ No settings
+{% endblock %}
diff --git a/src/Kore/RatingFieldTypeBundle/Storage/FieldType/Type.php b/src/Kore/RatingFieldTypeBundle/Storage/FieldType/Type.php
new file mode 100644
index 0000000000..b710d8243a
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Storage/FieldType/Type.php
@@ -0,0 +1,208 @@
+
+ * protected function createValueFromInput( $inputValue )
+ * {
+ * if ( is_array( $inputValue ) )
+ * {
+ * $inputValue = \eZ\Publish\Core\FieldType\CookieJar\Value( $inputValue );
+ * }
+ *
+ * return $inputValue;
+ * }
+ *
+ *
+ * @param mixed $inputValue
+ *
+ * @return mixed The potentially converted input value.
+ */
+ protected function createValueFromInput($inputValue)
+ {
+ // @EXT: Default possible depending on value class, at least for
+ // trivial values
+ //
+ // There is probably a connection with the edit template, which is
+ // missing in the tutoorial.
+ if ($inputValue instanceof Value) {
+ return $inputValue;
+ }
+
+ if (!is_numeric($inputValue)) {
+ return new Value(['rating' => false]);
+ }
+
+ return new Value(['rating' => (int) $inputValue]);
+ }
+
+ /**
+ * Throws an exception if value structure is not of expected format.
+ *
+ * Note that this does not include validation after the rules
+ * from validators, but only plausibility checks for the general data
+ * format.
+ *
+ * This is an operation method for {@see acceptValue()}.
+ *
+ * Example implementation:
+ *
+ * protected function checkValueStructure( Value $value )
+ * {
+ * if ( !is_array( $value->cookies ) )
+ * {
+ * throw new InvalidArgumentException( "An array of assorted cookies was expected." );
+ * }
+ * }
+ *
+ *
+ * @throws \eZ\Publish\API\Repository\Exceptions\InvalidArgumentException If the value does not match the expected structure.
+ *
+ * @param \eZ\Publish\Core\FieldType\Value $value
+ */
+ protected function checkValueStructure(CoreValue $value)
+ {
+ // @EXT: Default possible depending on value
+ if (!$value->rating) {
+ throw new InvalidArgumentException(
+ '$value->rating',
+ 'Expected rating to be a number'
+ );
+ }
+ }
+
+ /**
+ * Returns the empty value for this field type.
+ *
+ * This value will be used, if no value was provided for a field of this
+ * type and no default value was specified in the field definition. It is
+ * also used to determine that a user intentionally (or unintentionally)
+ * did not set a non-empty value.
+ *
+ * @return \eZ\Publish\SPI\FieldType\Value
+ */
+ public function getEmptyValue()
+ {
+ // @EXT: Default possible when we know about the value class
+ return new Value();
+ }
+
+ /**
+ * Returns a human readable string representation from the given $value.
+ *
+ * It will be used to generate content name and url alias if current field
+ * is designated to be used in the content name/urlAlias pattern.
+ *
+ * The used $value can be assumed to be already accepted by {@link *
+ * acceptValue()}.
+ *
+ * @deprecated Since 6.3/5.4.7, use \eZ\Publish\SPI\FieldType\Nameable
+ * @param \eZ\Publish\SPI\FieldType\Value $value
+ *
+ * @return string
+ */
+ public function getName(SPIValue $value)
+ {
+ return (string) $value;
+ }
+
+ /**
+ * Returns information for FieldValue->$sortKey relevant to the field type.
+ *
+ * Return value is mixed. It should be something which is sensible for
+ * sorting.
+ *
+ * It is up to the persistence implementation to handle those values.
+ * Common string and integer values are safe.
+ *
+ * For the legacy storage it is up to the field converters to set this
+ * value in either sort_key_string or sort_key_int.
+ *
+ * @param \eZ\Publish\Core\FieldType\Value $value
+ *
+ * @return mixed
+ */
+ protected function getSortInfo(CoreValue $value)
+ {
+ return $this->getName($value);
+ }
+
+ /**
+ * Converts an $hash to the Value defined by the field type.
+ *
+ * This is the reverse operation to {@link toHash()}. At least the hash
+ * format generated by {@link toHash()} must be converted in reverse.
+ * Additional formats might be supported in the rare case that this is
+ * necessary. See the class description for more details on a hash format.
+ *
+ * @param mixed $hash
+ *
+ * @return \eZ\Publish\SPI\FieldType\Value
+ */
+ public function fromHash($hash)
+ {
+ if ($hash === null) {
+ return $this->getEmptyValue();
+ }
+
+ // The default constructor at least works for the top level objects.
+ // For more complex values a manual conversion is necessary.
+ return new Value($hash);
+ }
+
+ /**
+ * Converts the given $value into a plain hash format.
+ *
+ * Converts the given $value into a plain hash format, which can be used to
+ * transfer the value through plain text formats, e.g. XML, which do not
+ * support complex structures like objects. See the class level doc block
+ * for additional information. See the class description for more details
+ * on a hash format.
+ *
+ * @param \eZ\Publish\SPI\FieldType\Value $value
+ *
+ * @return mixed
+ */
+ public function toHash(SPIValue $value)
+ {
+ // Simplest way to ensure a deep structure is cloned and converted into
+ // scalars and has maps.
+ return json_decode(json_encode($value), true);
+ }
+}
diff --git a/src/Kore/RatingFieldTypeBundle/Storage/FieldType/Value.php b/src/Kore/RatingFieldTypeBundle/Storage/FieldType/Value.php
new file mode 100644
index 0000000000..23bbc6bf35
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Storage/FieldType/Value.php
@@ -0,0 +1,15 @@
+rating;
+ }
+}
diff --git a/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Converter.php b/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Converter.php
new file mode 100644
index 0000000000..98d6afd5ef
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Converter.php
@@ -0,0 +1,42 @@
+dataText = json_encode($value->data);
+ $storageFieldValue->sortKeyString = $value->sortKey;
+ $storageFieldValue->sortKeyInt = $value->sortKey;
+ }
+
+ public function toFieldValue(StorageFieldValue $value, FieldValue $fieldValue)
+ {
+ $fieldValue->data = json_decode($value->dataText, true) ?: [];
+ $fieldValue->sortKey = $value->sortKeyInt ?? $value->sortKeyString;
+ }
+
+ public function toStorageFieldDefinition(FieldDefinition $fieldDef, StorageFieldDefinition $storageDef)
+ {
+ }
+
+ public function toFieldDefinition(StorageFieldDefinition $storageDef, FieldDefinition $fieldDef)
+ {
+ }
+
+ public function getIndexColumn()
+ {
+ // How to decide between sort_key_(int|string)
+ //
+ // How do we get access to the currently used value class to reason
+ // about this?
+ return 'sort_key_int';
+ }
+}
diff --git a/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Gateway.php b/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Gateway.php
new file mode 100644
index 0000000000..a66abe207e
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Gateway.php
@@ -0,0 +1,94 @@
+ []];
+
+ /**
+ * @var \Doctrine\DBAL\Connection
+ */
+ protected $connection;
+
+ public function __construct(Connection $connection)
+ {
+ $this->connection = $connection;
+ }
+
+ /**
+ * Stores the keyword list from $field->value->externalData.
+ *
+ * @param \eZ\Publish\SPI\Persistence\Content\Field
+ * @param int $contentTypeId
+ */
+ public function storeFieldData(Field $field, $contentTypeId)
+ {
+ $this->virtualDatabase[self::RATING_TABLE][$field->id] = $field->value->externalData;
+ }
+
+ /**
+ * Sets the list of assigned keywords into $field->value->externalData.
+ *
+ * @param \eZ\Publish\SPI\Persistence\Content\Field $field
+ */
+ public function getFieldData(Field $field)
+ {
+ $field->value->externalData = ['rating' => 3];
+
+ if (isset($this->virtualDatabase[self::RATING_TABLE][$field->id])) {
+ $field->value->externalData = $this->virtualDatabase[self::RATING_TABLE][$field->id];
+ }
+ }
+
+ /**
+ * Retrieve the ContentType ID for the given $field.
+ *
+ * @param \eZ\Publish\SPI\Persistence\Content\Field $field
+ *
+ * @return int
+ */
+ public function getContentTypeId(Field $field)
+ {
+ $query = $this->connection->createQueryBuilder();
+ $query
+ ->select($this->connection->quoteIdentifier('contentclass_id'))
+ ->from($this->connection->quoteIdentifier('ezcontentclass_attribute'))
+ ->where(
+ $query->expr()->eq('id', ':fieldDefinitionId')
+ )
+ ->setParameter(':fieldDefinitionId', $field->fieldDefinitionId);
+
+ $statement = $query->execute();
+
+ $row = $statement->fetch(\PDO::FETCH_ASSOC);
+
+ if ($row === false) {
+ throw new RuntimeException(
+ sprintf(
+ 'Content Type ID cannot be retrieved based on the field definition ID "%s"',
+ $field->fieldDefinitionId
+ )
+ );
+ }
+
+ return intval($row['contentclass_id']);
+ }
+
+ /**
+ * Deletes keyword data for the given $fieldId.
+ *
+ * @param int $fieldId
+ */
+ public function deleteFieldData($fieldId)
+ {
+ unset($this->virtualDatabase[self::RATING_TABLE][$fieldId]);
+ }
+}
diff --git a/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Storage.php b/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Storage.php
new file mode 100644
index 0000000000..d77b647279
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Storage/Legacy/Storage.php
@@ -0,0 +1,84 @@
+gateway->getContentTypeId($field);
+
+ return $this->gateway->storeFieldData($field, $contentTypeId);
+ }
+
+ /**
+ * Populates $field value property based on the external data.
+ * $field->value is a {@link eZ\Publish\SPI\Persistence\Content\FieldValue} object.
+ * This value holds the data as a {@link eZ\Publish\Core\FieldType\Value} based object,
+ * according to the field type (e.g. for TextLine, it will be a {@link eZ\Publish\Core\FieldType\TextLine\Value} object).
+ *
+ * @param \eZ\Publish\SPI\Persistence\Content\Field $field
+ * @param array $context
+ */
+ public function getFieldData(VersionInfo $versionInfo, Field $field, array $context)
+ {
+ // @todo: This should already retrieve the ContentType ID
+ return $this->gateway->getFieldData($field);
+ }
+
+ /**
+ * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $versionInfo
+ * @param array $fieldIds
+ * @param array $context
+ *
+ * @return bool
+ */
+ public function deleteFieldData(VersionInfo $versionInfo, array $fieldIds, array $context)
+ {
+ // If current version being asked to be deleted is not published, then don't delete keywords
+ // if there is some other version which is published (as keyword table is not versioned)
+ if ($versionInfo->status !== VersionInfo::STATUS_PUBLISHED &&
+ $versionInfo->contentInfo->isPublished
+ ) {
+ return false;
+ }
+
+ foreach ($fieldIds as $fieldId) {
+ $this->gateway->deleteFieldData($fieldId);
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks if field type has external data to deal with.
+ *
+ * @return bool
+ */
+ public function hasFieldData()
+ {
+ return true;
+ }
+
+ /**
+ * @param \eZ\Publish\SPI\Persistence\Content\VersionInfo $versionInfo
+ * @param \eZ\Publish\SPI\Persistence\Content\Field $field
+ * @param array $context
+ * @return \eZ\Publish\SPI\Search\Field[]|null
+ */
+ public function getIndexData(VersionInfo $versionInfo, Field $field, array $context)
+ {
+ return null;
+ }
+}
diff --git a/src/Kore/RatingFieldTypeBundle/Tests/TypeIntegrationTest.php b/src/Kore/RatingFieldTypeBundle/Tests/TypeIntegrationTest.php
new file mode 100644
index 0000000000..3349cae90a
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Tests/TypeIntegrationTest.php
@@ -0,0 +1,114 @@
+getHandler(
+ 'koreRating',
+ $fieldType,
+ new Converter(),
+ new Storage(
+ new Gateway(
+ $this->getDatabaseHandler()->getConnection()
+ )
+ )
+ );
+ }
+
+ /**
+ * Returns the FieldTypeConstraints to be used to create a field definition
+ * of the FieldType under test.
+ *
+ * @return \eZ\Publish\SPI\Persistence\Content\FieldTypeConstraints
+ */
+ public function getTypeConstraints()
+ {
+ return new Content\FieldTypeConstraints();
+ }
+
+ /**
+ * Returns the field definition data expected after loading the newly
+ * created field definition with the FieldType under test.
+ *
+ * This is a PHPUnit data provider
+ *
+ * @return array
+ */
+ public function getFieldDefinitionData()
+ {
+ return array(
+ // The ezkeyword field type does not have any special field definition
+ // properties
+ array('fieldType', 'koreRating'),
+ array('fieldTypeConstraints', new Content\FieldTypeConstraints()),
+ );
+ }
+
+ /**
+ * Get initial field value.
+ *
+ * @return \eZ\Publish\SPI\Persistence\Content\FieldValue
+ */
+ public function getInitialValue()
+ {
+ return new Content\FieldValue(
+ array(
+ 'data' => array(),
+ 'externalData' => array('rating' => 3),
+ 'sortKey' => '3',
+ )
+ );
+ }
+
+ /**
+ * Get update field value.
+ *
+ * Use to update the field
+ *
+ * @return \eZ\Publish\SPI\Persistence\Content\FieldValue
+ */
+ public function getUpdatedValue()
+ {
+ return new Content\FieldValue(
+ array(
+ 'data' => array(),
+ 'externalData' => array('rating' => 5),
+ 'sortKey' => '5',
+ )
+ );
+ }
+}
diff --git a/src/Kore/RatingFieldTypeBundle/Tests/TypeTest.php b/src/Kore/RatingFieldTypeBundle/Tests/TypeTest.php
new file mode 100644
index 0000000000..1ad84bc66d
--- /dev/null
+++ b/src/Kore/RatingFieldTypeBundle/Tests/TypeTest.php
@@ -0,0 +1,248 @@
+
+ * return array(
+ * array(
+ * new \stdClass(),
+ * 'eZ\\Publish\\Core\\Base\\Exceptions\\InvalidArgumentException',
+ * ),
+ * array(
+ * array(),
+ * 'eZ\\Publish\\Core\\Base\\Exceptions\\InvalidArgumentException',
+ * ),
+ * // ...
+ * );
+ *
+ *
+ * @return array
+ */
+ public function provideInvalidInputForAcceptValue()
+ {
+ return array(
+ array(
+ [],
+ 'eZ\\Publish\\Core\\Base\\Exceptions\\InvalidArgumentException',
+ ),
+ );
+ }
+
+ /**
+ * Data provider for valid input to acceptValue().
+ *
+ * Returns an array of data provider sets with 2 arguments: 1. The valid
+ * input to acceptValue(), 2. The expected return value from acceptValue().
+ * For example:
+ *
+ *
+ * return array(
+ * array(
+ * null,
+ * null
+ * ),
+ * array(
+ * __FILE__,
+ * new BinaryFileValue( array(
+ * 'path' => __FILE__,
+ * 'fileName' => basename( __FILE__ ),
+ * 'fileSize' => filesize( __FILE__ ),
+ * 'downloadCount' => 0,
+ * 'mimeType' => 'text/plain',
+ * ) )
+ * ),
+ * // ...
+ * );
+ *
+ *
+ * @return array
+ */
+ public function provideValidInputForAcceptValue()
+ {
+ return array(
+ array(
+ null,
+ new Value(),
+ ),
+ array(
+ 1,
+ new Value(['rating' => 1]),
+ ),
+ array(
+ '2',
+ new Value(['rating' => 2]),
+ ),
+ );
+ }
+
+ /**
+ * Provide input for the toHash() method.
+ *
+ * Returns an array of data provider sets with 2 arguments: 1. The valid
+ * input to toHash(), 2. The expected return value from toHash().
+ * For example:
+ *
+ *
+ * return array(
+ * array(
+ * null,
+ * null
+ * ),
+ * array(
+ * new BinaryFileValue( array(
+ * 'path' => 'some/file/here',
+ * 'fileName' => 'sindelfingen.jpg',
+ * 'fileSize' => 2342,
+ * 'downloadCount' => 0,
+ * 'mimeType' => 'image/jpeg',
+ * ) ),
+ * array(
+ * 'path' => 'some/file/here',
+ * 'fileName' => 'sindelfingen.jpg',
+ * 'fileSize' => 2342,
+ * 'downloadCount' => 0,
+ * 'mimeType' => 'image/jpeg',
+ * )
+ * ),
+ * // ...
+ * );
+ *
+ *
+ * @return array
+ */
+ public function provideInputForToHash()
+ {
+ return array(
+ array(
+ new Value(),
+ ['rating' => 3],
+ ),
+ array(
+ new Value(['rating' => 5]),
+ ['rating' => 5],
+ ),
+ );
+ }
+
+ /**
+ * Provide input to fromHash() method.
+ *
+ * Returns an array of data provider sets with 2 arguments: 1. The valid
+ * input to fromHash(), 2. The expected return value from fromHash().
+ * For example:
+ *
+ *
+ * return array(
+ * array(
+ * null,
+ * null
+ * ),
+ * array(
+ * array(
+ * 'path' => 'some/file/here',
+ * 'fileName' => 'sindelfingen.jpg',
+ * 'fileSize' => 2342,
+ * 'downloadCount' => 0,
+ * 'mimeType' => 'image/jpeg',
+ * ),
+ * new BinaryFileValue( array(
+ * 'path' => 'some/file/here',
+ * 'fileName' => 'sindelfingen.jpg',
+ * 'fileSize' => 2342,
+ * 'downloadCount' => 0,
+ * 'mimeType' => 'image/jpeg',
+ * ) )
+ * ),
+ * // ...
+ * );
+ *
+ *
+ * @return array
+ */
+ public function provideInputForFromHash()
+ {
+ return array(
+ array(
+ array(),
+ new Value(array()),
+ ),
+ array(
+ ['rating' => 5],
+ new Value(['rating' => 5]),
+ ),
+ );
+ }
+
+ protected function provideFieldTypeIdentifier()
+ {
+ return 'koreRating';
+ }
+
+ public function provideDataForGetName()
+ {
+ return array(
+ array($this->getEmptyValueExpectation(), '3'),
+ array(new Value(['rating' => 5]), '5'),
+ );
+ }
+}