symphony/lib/toolkit/class.field.php
<?php
/**
* @package toolkit
*/
/**
* The Field class represents a Symphony Field object. Fields are the building
* blocks for Sections. All fields instances are unique and can only be used once
* in a Symphony install. Fields have their own field table which records where
* instances of this field type have been used in other sections and their settings.
* They also spinoff other `tbl_entry_data_{id}` tables that actually store data for
* entries particular to this field.
*
* @since Symphony 3.0.0 it implements the ArrayAccess interface.
*/
class Field implements ArrayAccess
{
/**
* The desired result when creating a field in the section editor
* @var integer
*/
const __OK__ = 100;
/**
* If an error occurring when saving a section because of this field,
* this will be returned
* @var integer
*/
const __ERROR__ = 150;
/**
* When saving a section, if a value that is required is missing,
* this will be returned
* @var integer
*/
const __MISSING_FIELDS__ = 200;
/**
* If a value for a setting is invalid, this will be returned
* @var integer
*/
const __INVALID_FIELDS__ = 220;
/**
* If there already is an instance of this field in this section and
* `mustBeUnique()` returns true, this will be returned
* @var integer
* @see mustBeUnique()
*/
const __DUPLICATE__ = 300;
/**
* Fields can returned this is an error occurred when saving the
* field's settings that doesn't fit another `Field` constant
* @var integer
*/
const __ERROR_CUSTOM__ = 400;
/**
* If the field name is not a valid QName, this error will be returned
* @var integer
*/
const __INVALID_QNAME__ = 500;
/**
* Used by the `FieldManager` to return fields that can be toggled
* @var integer
*/
const __TOGGLEABLE_ONLY__ = 600;
/**
* Used by the `FieldManager` to return fields that can't be toggled
* @var integer
*/
const __UNTOGGLEABLE_ONLY__ = 700;
/**
* Used by the `FieldManager` to return fields that can be filtered
* @var integer
*/
const __FILTERABLE_ONLY__ = 800;
/**
* Used by the `FieldManager` to return fields that can't be filtered
* @var integer
*/
const __UNFILTERABLE_ONLY__ = 900;
/**
* Used by the `FieldManager` to just return all fields
* @var integer
*/
const __FIELD_ALL__ = 1000;
/**
* Used to manage the joins when this field used in a datasource
* @deprecated @since Symphony 3.0.0
* @var integer
*/
protected $_key = 0;
/**
* The handle of this field object
* @var string
*/
protected $_handle = null;
/**
* The name of this field object
* @var string
*/
protected $_name = null;
/**
* An associative array of the settings for this `Field` instance
* @var array
*/
protected $_settings = array();
/**
* Whether this field is required inherently, defaults to false.
* @var boolean
*/
protected $_required = false;
/**
* Whether this field can be viewed on the entries table. Note
* that this is not the same variable as the one set when saving
* a field in the section editor, rather just the if the field has
* the ability to be shown. Defaults to true.
* @var boolean
*/
protected $_showcolumn = true;
/**
* Whether this field has an association that should be shown on
* the Publish Index. This does not mean that it will be, but just
* that this field has the ability too. Defaults to false.
* @var boolean
*/
protected $_showassociation = false;
/**
* The entry query field adapter object, responsible for filter and sort.
* The default class does not set a default EntryQueryFieldAdapter object
* to allow the compatibility layer to work. In later versions, this can change.
* @since Symphony 3.0.0
* @var EntryQueryFieldAdapter
*/
protected $entryQueryFieldAdapter = null;
/**
* Construct a new instance of this field.
*/
public function __construct()
{
$this->_handle = (strtolower(get_class($this)) == 'field' ? 'field' : strtolower(substr(get_class($this), 5)));
}
/**
* Adjust the newly cloned field in order to fix circular references.
*
* @return void
*/
public function __clone()
{
if ($this->entryQueryFieldAdapter) {
$eqfaClass = get_class($this->entryQueryFieldAdapter);
$this->entryQueryFieldAdapter = new $eqfaClass($this);
}
}
/**
* Test whether this field can show the table column.
*
* @return boolean
* true if this can, false otherwise.
*/
public function canShowTableColumn()
{
return $this->_showcolumn;
}
/**
* Test whether this field can show the association column in
* the Publish Index.
*
* @since Symphony 2.6.0
* @return boolean
* true if this can, false otherwise.
*/
public function canShowAssociationColumn()
{
return $this->_showassociation;
}
/**
* Test whether this field can be toggled using the With Selected menu
* on the Publish Index.
*
* @return boolean
* true if it can be toggled, false otherwise.
*/
public function canToggle()
{
return false;
}
/**
* Accessor to the toggle states. This default implementation returns
* an empty array.
*
* @return array
* the array of toggle states.
*/
public function getToggleStates()
{
return array();
}
/**
* Toggle the field data. This default implementation always returns
* the input data.
*
* @param array $data
* the data to toggle.
* @param string $newState
* the new value to set
* @param integer $entry_id (optional)
* an optional entry ID for more intelligent processing. defaults to null
* @return array
* the toggled data.
*/
public function toggleFieldData(array $data, $newState, $entry_id = null)
{
return $data;
}
/**
* Test whether this field can be filtered. This default implementation
* prohibits filtering. Filtering allows the XML output results to be limited
* according to an input parameter. Subclasses should override this if
* filtering is supported.
*
* @return boolean
* true if this can be filtered, false otherwise.
*/
public function canFilter()
{
return false;
}
/**
* Test whether this field can be filtered in the publish index. This default
* implementation prohibts filtering. Publish Filtering allows the index view
* to filter results. Subclasses should override this if
* filtering is supported.
*
* @return boolean
* true if this can be publish-filtered, false otherwise.
*/
public function canPublishFilter()
{
return $this->canFilter();
}
/**
* Test whether this field can be prepopulated with data. This default
* implementation does not support pre-population and, thus, returns false.
*
* @return boolean
* true if this can be pre-populated, false otherwise.
*/
public function canPrePopulate()
{
return false;
}
/**
* Test whether this field can be sorted. This default implementation
* returns false.
*
* @return boolean
* true if this field is sortable, false otherwise.
*/
public function isSortable()
{
return false;
}
/**
* Test whether this field must be unique in a section, that is, only one of
* this field's type is allowed per section. This default implementation
* always returns false.
*
* @return boolean
* true if the content of this field must be unique, false otherwise.
*/
public function mustBeUnique()
{
return false;
}
/**
* Test whether this field supports data source output grouping. This
* default implementation prohibits grouping. Data-source grouping allows
* clients of this field to group the XML output according to this field.
* Subclasses should override this if grouping is supported.
*
* @return boolean
* true if this field does support data source grouping, false otherwise.
*/
public function allowDatasourceOutputGrouping()
{
return false;
}
/**
* Test whether this field requires grouping. If this function returns true
* SQL statements generated in the `EntryManager` will include the `DISTINCT` keyword
* to only return a single row for an entry regardless of how many 'matches' it
* might have. This default implementation returns false.
*
* @return boolean
* true if this field requires grouping, false otherwise.
*/
public function requiresSQLGrouping()
{
return false;
}
/**
* Test whether this field supports data source parameter output. This
* default implementation prohibits parameter output. Data-source
* parameter output allows this field to be provided as a parameter
* to other data sources or XSLT. Subclasses should override this if
* parameter output is supported.
*
* @return boolean
* true if this supports data source parameter output, false otherwise.
*/
public function allowDatasourceParamOutput()
{
return false;
}
/**
* Accessor to the handle of this field object. The Symphony convention is
* for field subclass names to be prefixed with field. Handle removes this prefix
* so that the class handle can be used as the field type.
*
* @return string
* The field classname minus the field prefix.
*/
public function handle()
{
return $this->_handle;
}
/**
* Accessor to the name of this field object. The name may contain characters
* that normally would be stripped in the handle while also allowing the field
* name to be localized. If a name is not set, it will return the handle of the
* the field
*
* @return string
* The field name
*/
public function name()
{
return ($this->_name ? $this->_name : $this->_handle);
}
/**
* Clean the input value using html entity encode.
*
* @param mixed $value
* the value to clean.
* @return string
* the cleaned value.
*/
public function cleanValue($value)
{
return html_entity_decode($value);
}
/**
* Fields have settings that define how that field will act in a section, including
* if it's required, any validators, if it can be shown on the entries table etc. This
* function will set a setting to a value. This function will set a setting to a value
* overwriting any existing value for this setting
*
* @param string $setting
* the setting key.
* @param mixed $value
* the value of the setting.
*/
public function set($setting, $value)
{
$this->_settings[$setting] = $value;
}
/**
* Add or overwrite the settings of this field by providing an associative array
* of the settings. This will do nothing if the input array is empty. If a setting is
* omitted from the input array, it will not be unset by this function
*
* @param array $array
* the associative array of settings for this field
*/
public function setArray(array $array = array())
{
if (empty($array)) {
return;
}
foreach ($array as $setting => $value) {
$this->set($setting, $value);
}
}
/**
* Fill the input data array with default values for known keys provided
* these settings are not already set. The input array is then used to set
* the values of the corresponding settings for this field. This function
* is called when a section is saved.
*
* @param array $settings
* the data array to initialize if necessary.
*/
public function setFromPOST(array $settings = array())
{
$settings['location'] = (isset($settings['location']) ? $settings['location'] : 'main');
$settings['required'] = (isset($settings['required']) && $settings['required'] === 'yes' ? 'yes' : 'no');
$settings['show_column'] = (isset($settings['show_column']) && $settings['show_column'] === 'yes' ? 'yes' : 'no');
$this->setArray($settings);
}
/**
* Accessor to the a setting by name. If no setting is provided all the
* settings of this `Field` instance are returned.
*
* @param string $setting (optional)
* the name of the setting to access the value for. This is optional and
* defaults to null in which case all settings are returned.
* @return null|mixed|array
* the value of the setting if there is one, all settings if the input setting
* was omitted or null if the setting was supplied but there is no value
* for that setting.
*/
public function get($setting = null)
{
if (is_null($setting)) {
return $this->_settings;
}
if (!isset($this->_settings[$setting])) {
return null;
}
return $this->_settings[$setting];
}
/**
* Unset the value of a setting by the key
*
* @param string $setting
* the key of the setting to unset.
*/
public function remove($setting)
{
unset($this->_settings[$setting]);
}
/**
* Implementation of ArrayAccess::offsetExists()
*
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset)
{
return isset($this->_settings[$offset]);
}
/**
* Implementation of ArrayAccess::offsetGet()
*
* @param mixed $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->_settings[$offset];
}
/**
* Implementation of ArrayAccess::offsetSet()
*
* @param mixed $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value)
{
$this->_settings[$offset] = $value;
}
/**
* Implementation of ArrayAccess::offsetUnset()
*
* @param mixed $offset
* @return void
*/
public function offsetUnset($offset)
{
unset($this->_settings[$offset]);
}
/**
* Getter for this field's EntryQuery operations object.
*
* @since Symphony 3.0.0
* @return EntryQueryFieldAdapter
*/
public function getEntryQueryFieldAdapter()
{
return $this->entryQueryFieldAdapter;
}
/**
* Just prior to the field being deleted, this function allows
* Fields to cleanup any additional things before it is removed
* from the section. This may be useful to remove data from any
* custom field tables or the configuration.
*
* @since Symphony 2.2.1
* @return boolean
*/
public function tearDown()
{
return true;
}
/**
* Allows a field to set default settings.
*
* @param array $settings
* the array of settings to populate with their defaults.
*/
public function findDefaults(array &$settings)
{
}
/**
* Display the default settings panel, calls the `buildSummaryBlock`
* function after basic field settings are added to the wrapper.
*
* @see buildSummaryBlock()
* @param XMLElement $wrapper
* the input XMLElement to which the display of this will be appended.
* @param mixed $errors
* the input error collection. this defaults to null.
* @throws InvalidArgumentException
*/
public function displaySettingsPanel(XMLElement &$wrapper, $errors = null)
{
// Create header
$location = ($this->get('location') ? $this->get('location') : 'main');
$header = new XMLElement('header', null, array(
'class' => 'frame-header ' . $location,
'data-name' => $this->name(),
'title' => $this->get('id'),
));
$label = (($this->get('label')) ? $this->get('label') : __('New Field'));
$header->appendChild(new XMLElement('h4', '<strong>' . $label . '</strong> <span class="type">' . $this->name() . '</span>'));
$wrapper->appendChild($header);
// Create content
$wrapper->appendChild(Widget::Input('fields['.$this->get('sortorder').'][type]', $this->handle(), 'hidden'));
if ($this->get('id')) {
$wrapper->appendChild(Widget::Input('fields['.$this->get('sortorder').'][id]', $this->get('id'), 'hidden'));
}
$wrapper->appendChild($this->buildSummaryBlock($errors));
}
/**
* Construct the html block to display a summary of this field, which is the field
* Label and it's location within the section. Any error messages generated are
* appended to the optional input error array. This function calls
* `buildLocationSelect` once it is completed
*
* @see buildLocationSelect()
* @param array $errors (optional)
* an array to append html formatted error messages to. this defaults to null.
* @throws InvalidArgumentException
* @return XMLElement
* the root XML element of the html display of this.
*/
public function buildSummaryBlock($errors = null)
{
$div = new XMLElement('div');
// Publish label
$label = Widget::Label(__('Label'));
$label->appendChild(
Widget::Input('fields['.$this->get('sortorder').'][label]', $this->get('label'))
);
if (isset($errors['label'])) {
$div->appendChild(Widget::Error($label, $errors['label']));
} else {
$div->appendChild($label);
}
// Handle + placement
$group = new XMLElement('div');
$group->setAttribute('class', 'two columns');
$label = Widget::Label(__('Handle'));
$label->setAttribute('class', 'column');
$label->appendChild(Widget::Input('fields['.$this->get('sortorder').'][element_name]', $this->get('element_name')));
if (isset($errors['element_name'])) {
$group->appendChild(Widget::Error($label, $errors['element_name']));
} else {
$group->appendChild($label);
}
// Location
$group->appendChild($this->buildLocationSelect($this->get('location'), 'fields['.$this->get('sortorder').'][location]'));
$div->appendChild($group);
return $div;
}
/**
* Build the location select widget. This widget allows users to select
* whether this field will appear in the main content column or in the sidebar
* when creating a new entry.
*
* @param string|null $selected (optional)
* the currently selected location, if there is one. this defaults to null.
* @param string $name (optional)
* the name of this field. this is optional and defaults to `fields[location]`.
* @param string $label_value (optional)
* any predefined label for this widget. this is an optional argument that defaults
* to null.
* @throws InvalidArgumentException
* @return XMLElement
* An XMLElement representing a `<select>` field containing the options.
*/
public function buildLocationSelect($selected = null, $name = 'fields[location]', $label_value = null)
{
if (!$label_value) {
$label_value = __('Placement');
}
$label = Widget::Label($label_value);
$label->setAttribute('class', 'column');
$options = array(
array('main', $selected == 'main', __('Main content')),
array('sidebar', $selected == 'sidebar', __('Sidebar'))
);
$label->appendChild(Widget::Select($name, $options));
return $label;
}
/**
* Construct the html widget for selecting a text formatter for this field.
*
* @param string $selected (optional)
* the currently selected text formatter name if there is one. this defaults
* to null.
* @param string $name (optional)
* the name of this field in the form. this is optional and defaults to
* "fields[format]".
* @param string $label_value
* the default label for the widget to construct. if null is passed in then
* this defaults to the localization of "Formatting".
* @throws InvalidArgumentException
* @return XMLElement
* An XMLElement representing a `<select>` field containing the options.
*/
public function buildFormatterSelect($selected = null, $name = 'fields[format]', $label_value)
{
$formatters = TextformatterManager::listAll();
if (!$label_value) {
$label_value = __('Formatting');
}
$label = Widget::Label($label_value);
$label->setAttribute('class', 'column');
$options = array();
$options[] = array('none', false, __('None'));
if (!empty($formatters) && is_array($formatters)) {
foreach ($formatters as $handle => $about) {
$options[] = array($handle, ($selected == $handle), $about['name']);
}
}
$label->appendChild(Widget::Select($name, $options));
return $label;
}
/**
* Append a validator selector to a given `XMLElement`. Note that this
* function differs from the other two similarly named build functions in
* that it takes an `XMLElement` to append the Validator to as a parameter,
* and does not return anything.
*
* @param XMLElement $wrapper
* the parent element to append the XMLElement of the Validation select to,
* passed by reference.
* @param string $selected (optional)
* the current validator selection if there is one. defaults to null if there
* isn't.
* @param string $name (optional)
* the form element name of this field. this defaults to "fields[validator]".
* @param string $type (optional)
* the type of input for the validation to apply to. this defaults to 'input'
* but also accepts 'upload'.
* @param array $errors (optional)
* an associative array of errors
* @throws InvalidArgumentException
*/
public function buildValidationSelect(XMLElement &$wrapper, $selected = null, $name = 'fields[validator]', $type = 'input', array $errors = null)
{
include TOOLKIT . '/util.validators.php';
$rules = ($type == 'upload' ? $upload : $validators);
$label = Widget::Label(__('Validation Rule'));
$label->setAttribute('class', 'column');
$label->appendChild(new XMLElement('i', __('Optional')));
$label->appendChild(Widget::Input($name, $selected));
$ul = new XMLElement('ul', null, array('class' => 'tags singular', 'data-interactive' => 'data-interactive'));
foreach ($rules as $name => $rule) {
$ul->appendChild(new XMLElement('li', $name, array('class' => $rule)));
}
if (isset($errors['validator'])) {
$div = new XMLElement('div');
$div->appendChild($label);
$div->appendChild($ul);
$wrapper->appendChild(Widget::Error($div, $errors['validator']));
} else {
$wrapper->appendChild($label);
$wrapper->appendChild($ul);
}
}
/**
* Append the html widget for selecting an association interface and editor
* for this field.
*
* @param XMLElement $wrapper
* the parent XML element to append the association interface selection to,
* if either interfaces or editors are provided to the system.
* @since Symphony 2.5.0
*/
public function appendAssociationInterfaceSelect(XMLElement &$wrapper)
{
$wrapper->setAttribute('data-condition', 'associative');
$interfaces = Symphony::ExtensionManager()->getProvidersOf(iProvider::ASSOCIATION_UI);
$editors = Symphony::ExtensionManager()->getProvidersOf(iProvider::ASSOCIATION_EDITOR);
if (!empty($interfaces) || !empty($editors)) {
$association_context = $this->getAssociationContext();
$group = new XMLElement('div');
if (!empty($interfaces) && !empty($editors)) {
$group->setAttribute('class', 'two columns');
}
// Create interface select
if (!empty($interfaces)) {
$label = Widget::Label(__('Association Interface'), null, 'column');
$label->appendChild(new XMLElement('i', __('Optional')));
$options = array(
array(null, false, __('None'))
);
foreach ($interfaces as $id => $name) {
$options[] = array($id, ($association_context['interface'] === $id), $name);
}
$select = Widget::Select('fields[' . $this->get('sortorder') . '][association_ui]', $options);
$label->appendChild($select);
$group->appendChild($label);
}
// Create editor select
if (!empty($editors)) {
$label = Widget::Label(__('Association Editor'), null, 'column');
$label->appendChild(new XMLElement('i', __('Optional')));
$options = array(
array(null, false, __('None'))
);
foreach ($editors as $id => $name) {
$options[] = array($id, ($association_context['editor'] === $id), $name);
}
$select = Widget::Select('fields[' . $this->get('sortorder') . '][association_editor]', $options);
$label->appendChild($select);
$group->appendChild($label);
}
$wrapper->appendChild($group);
}
}
/**
* Get association data of the current field from the page context.
*
* @since Symphony 2.5.0
* @return array
*/
public function getAssociationContext()
{
$context = Symphony::Engine()->Page->getContext();
$associations = $context['associations']['parent'];
$field_association = array();
$count = 0;
if (!empty($associations)) {
$associationsCount = count($associations);
for ($i = 0; $i < $associationsCount; $i++) {
if ($associations[$i]['child_section_field_id'] == $this->get('id')) {
if ($count === 0) {
$field_association = $associations[$i];
$count++;
} else {
$field_association['parent_section_id'] .= '|' . $associations[$i]['parent_section_id'];
$field_association['parent_section_field_id'] .= '|' . $associations[$i]['parent_section_field_id'];
}
}
}
}
return $field_association;
}
/**
* Set association data for the current field.
*
* @since Symphony 2.5.0
* @param XMLElement $wrapper
*/
public function setAssociationContext(XMLElement &$wrapper)
{
$association_context = $this->getAssociationContext();
if (!empty($association_context)) {
$wrapper->setAttributeArray(array(
'data-parent-section-id' => $association_context['parent_section_id'],
'data-parent-section-field-id' => $association_context['parent_section_field_id'],
'data-child-section-id' => $association_context['child_section_id'],
'data-child-section-field-id' => $association_context['child_section_field_id'],
'data-interface' => $association_context['interface'],
'data-editor' => $association_context['editor']
));
}
}
/**
* Append and set a labeled html checkbox to the input XML element if this
* field is set as a required field.
*
* @param XMLElement $wrapper
* the parent XML element to append the constructed html checkbox to if
* necessary.
* @throws InvalidArgumentException
*/
public function appendRequiredCheckbox(XMLElement &$wrapper)
{
if (!$this->_required) {
return;
}
$this->createCheckboxSetting($wrapper, 'required', __('Make this a required field'));
}
/**
* Append the show column html widget to the input parent XML element. This
* displays a column in the entries table or not.
*
* @param XMLElement $wrapper
* the parent XML element to append the checkbox to.
* @throws InvalidArgumentException
*/
public function appendShowColumnCheckbox(XMLElement &$wrapper)
{
if (!$this->_showcolumn) {
return;
}
$this->createCheckboxSetting($wrapper, 'show_column', __('Display in entries table'));
}
/**
* Append the show association html widget to the input parent XML element. This
* widget allows fields that provide linking to hide or show the column in the linked
* section, similar to how the Show Column functionality works, but for the linked
* section.
*
* @param XMLElement $wrapper
* the parent XML element to append the checkbox to.
* @param string $help (optional)
* a help message to show below the checkbox.
* @throws InvalidArgumentException
*/
public function appendShowAssociationCheckbox(XMLElement &$wrapper, $help = null)
{
if (!$this->_showassociation) {
return;
}
$label = $this->createCheckboxSetting($wrapper, 'show_association', __('Display associations in entries table'), $help);
$label->setAttribute('data-condition', 'associative');
}
/**
* Given the setting name and the label, this helper method will add
* the required markup for a checkbox to the given `$wrapper`.
*
* @since Symphony 2.5.2
* @param XMLElement $wrapper
* Passed by reference, this will have the resulting markup appended to it
* @param string $setting
* This will be used with $this->get() to get the existing value
* @param string $label_description
* This will be localisable and displayed after the checkbox when
* generated.
* @param string $help (optional)
* A help message to show below the checkbox.
* @return XMLElement
* The Label and Checkbox that was just added to the `$wrapper`.
*/
public function createCheckboxSetting(XMLElement &$wrapper, $setting, $label_description, $help = null)
{
$order = $this->get('sortorder');
$name = "fields[$order][$setting]";
$label = Widget::Checkbox($name, $this->get($setting), $label_description, $wrapper, $help);
$label->addClass('column');
return $label;
}
/**
* Append the default status footer to the field settings panel.
* Displays the required and show column checkboxes.
*
* @param XMLElement $wrapper
* the parent XML element to append the checkbox to.
* @throws InvalidArgumentException
*/
public function appendStatusFooter(XMLElement &$wrapper)
{
$fieldset = new XMLElement('fieldset');
$div = new XMLElement('div', null, array('class' => 'two columns'));
$this->appendRequiredCheckbox($div);
$this->appendShowColumnCheckbox($div);
$fieldset->appendChild($div);
$wrapper->appendChild($fieldset);
}
/**
* Check the field's settings to ensure they are valid on the section
* editor
*
* @param array $errors
* the array to populate with the errors found.
* @param boolean $checkForDuplicates (optional)
* if set to true, duplicate Field name's in the same section will be flagged
* as errors. Defaults to true.
* @return integer
* returns the status of the checking. if errors has been populated with
* any errors `self::__ERROR__`, `self::__OK__` otherwise.
*/
public function checkFields(array &$errors, $checkForDuplicates = true)
{
$parent_section = $this->get('parent_section');
$label = $this->get('label');
$element_name = $this->get('element_name');
if ($label === '') {
$errors['label'] = __('This is a required field.');
} elseif (strtolower($label) === 'id') {
$errors['label'] = __('%s is a reserved name used by the system and is not allowed for a field handle. Try using %s instead.', array('<code>ID</code>', '<code>UID</code>'));
// Check label starts with a letter
} elseif (!preg_match('/^\p{L}/u', $label)) {
$errors['label'] = __('The label of the field must begin with a letter.');
}
if ($element_name === '') {
$errors['element_name'] = __('This is a required field.');
} elseif ($element_name === 'id') {
$errors['element_name'] = __('%s is a reserved name used by the system and is not allowed for a field handle. Try using %s instead.', array('<code>id</code>', '<code>uid</code>'));
} elseif (!preg_match('/^[a-z]/i', $element_name)) {
$errors['element_name'] = __('Invalid element name. Must be valid %s.', array('<code>QName</code>'));
} elseif ($checkForDuplicates) {
if (FieldManager::fetchFieldIDFromElementName($element_name, $parent_section) !== $this->get('id')) {
$errors['element_name'] = __('A field with that element name already exists. Please choose another.');
}
}
// Check that if the validator is provided that it's a valid regular expression
if (!is_null($this->get('validator')) && $this->get('validator') !== '') {
if (@preg_match($this->get('validator'), 'teststring') === false) {
$errors['validator'] = __('Validation rule is not a valid regular expression');
}
}
return (!empty($errors) ? self::__ERROR__ : self::__OK__);
}
/**
* Format this field value for display in the publish index tables.
*
* Since Symphony 2.5.0, this function will call `Field::prepareReadableValue`
* in order to get the field's human readable value.
*
* @param array $data
* an associative array of data for this string. At minimum this requires a
* key of 'value'.
* @param XMLElement $link (optional)
* an XML link structure to append the content of this to provided it is not
* null. it defaults to null.
* @param integer $entry_id (optional)
* An option entry ID for more intelligent processing. defaults to null
* @return string
* the formatted string summary of the values of this field instance.
*/
public function prepareTableValue($data, XMLElement $link = null, $entry_id = null)
{
$value = $this->prepareReadableValue($data, $entry_id, true, __('None'));
if ($link) {
$link->setValue($value);
return $link->generate();
}
return $value;
}
/**
* Format this field value for display as readable text value. By default, it
* will call `Field::prepareTextValue` to get the raw text value of this field.
*
* If $truncate is set to true, Symphony will truncate the value to the
* configuration setting `cell_truncation_length`.
*
* @since Symphony 2.5.0
* @param array $data
* an associative array of data for this string. At minimum this requires a
* key of 'value'.
* @param integer $entry_id (optional)
* An option entry ID for more intelligent processing. Defaults to null.
* @param string $defaultValue (optional)
* The value to use when no plain text representation of the field's data
* can be made. Defaults to null.
* @return string
* the readable text summary of the values of this field instance.
*/
public function prepareReadableValue($data, $entry_id = null, $truncate = false, $defaultValue = null)
{
$value = $this->prepareTextValue($data, $entry_id);
if ($truncate) {
$max_length = Symphony::Configuration()->get('cell_truncation_length', 'symphony');
$max_length = ($max_length ? $max_length : 75);
$value = (General::strlen($value) <= $max_length ? $value : General::substr($value, 0, $max_length) . 'ā¦');
}
if (General::strlen($value) == 0 && $defaultValue != null) {
$value = $defaultValue;
}
return $value;
}
/**
* Format this field value for complete display as text (string). By default,
* it looks for the 'value' key in the $data array and strip tags from it.
*
* @since Symphony 2.5.0
* @param array $data
* an associative array of data for this string. At minimum this requires a
* key of 'value'.
* @param integer $entry_id (optional)
* An option entry ID for more intelligent processing. defaults to null
* @return string
* the complete text representation of the values of this field instance.
*/
public function prepareTextValue($data, $entry_id = null)
{
return strip_tags($data['value']);
}
/**
* This is general purpose factory method that makes it easier to create the
* markup needed in order to create an Associations Drawer XMLElement.
*
* @since Symphony 2.5.0
*
* @param string $value
* The value to display in the link
* @param Entry $e
* The associated entry
* @param array $parent_association
* An array containing information about the association
* @param string $prepopulate
* A string containing prepopulate parameter to append to the association url
*
* @return XMLElement
* The XMLElement must be a li node, since it will be added an ul node.
*/
public static function createAssociationsDrawerXMLElement($value, Entry $e, array $parent_association, $prepopulate = '')
{
$li = new XMLElement('li');
$a = new XMLElement('a', $value);
$a->setAttribute('href', SYMPHONY_URL . '/publish/' . $parent_association['handle'] . '/edit/' . $e->get('id') . '/' . $prepopulate);
$li->appendChild($a);
return $li;
}
/**
* Format this field value for display in the Associations Drawer publish index.
* By default, Symphony will use the return value of the `prepareReadableValue` function.
*
* @since Symphony 2.4
* @since Symphony 2.5.0 The prepopulate parameter was added.
*
* @param Entry $e
* The associated entry
* @param array $parent_association
* An array containing information about the association
* @param string $prepopulate
* A string containing prepopulate parameter to append to the association url
*
* @return XMLElement
* The XMLElement must be a li node, since it will be added an ul node.
*/
public function prepareAssociationsDrawerXMLElement(Entry $e, array $parent_association, $prepopulate = '')
{
$value = $this->prepareReadableValue($e->getData($this->get('id')), $e->get('id'));
// fallback for compatibility since the default
// `preparePlainTextValue` is not compatible with all fields
// this should be removed in Symphony 3.0
if (empty($value)) {
$value = strip_tags($this->prepareTableValue($e->getData($this->get('id')), null, $e->get('id')));
}
// use our factory method to create the html
$li = self::createAssociationsDrawerXMLElement($value, $e, $parent_association, $prepopulate);
$li->setAttribute('class', 'field-' . $this->get('type'));
return $li;
}
/**
* Display the publish panel for this field. The display panel is the
* interface shown to Authors that allow them to input data into this
* field for an `Entry`.
*
* @param XMLElement $wrapper
* the XML element to append the html defined user interface to this
* field.
* @param array $data (optional)
* any existing data that has been supplied for this field instance.
* this is encoded as an array of columns, each column maps to an
* array of row indexes to the contents of that column. this defaults
* to null.
* @param mixed $flagWithError (optional)
* flag with error defaults to null.
* @param string $fieldnamePrefix (optional)
* the string to be prepended to the display of the name of this field.
* this defaults to null.
* @param string $fieldnamePostfix (optional)
* the string to be appended to the display of the name of this field.
* this defaults to null.
* @param integer $entry_id (optional)
* the entry id of this field. this defaults to null.
*/
public function displayPublishPanel(XMLElement &$wrapper, $data = null, $flagWithError = null, $fieldnamePrefix = null, $fieldnamePostfix = null, $entry_id = null)
{
}
/**
* Check the field data that has been posted from a form. This will set the
* input message to the error message or to null if there is none. Any existing
* message value will be overwritten.
*
* @param array $data
* the input data to check.
* @param string $message
* the place to set any generated error message. any previous value for
* this variable will be overwritten.
* @param integer $entry_id (optional)
* the optional id of this field entry instance. this defaults to null.
* @return integer
* `self::__MISSING_FIELDS__` if there are any missing required fields,
* `self::__OK__` otherwise.
*/
public function checkPostFieldData($data, &$message, $entry_id = null)
{
$message = null;
$has_no_value = is_array($data) ? empty($data) : strlen(trim($data)) == 0;
if ($this->get('required') === 'yes' && $has_no_value) {
$message = __('ā%sā is a required field.', array($this->get('label')));
return self::__MISSING_FIELDS__;
}
return self::__OK__;
}
/**
* Process the raw field data.
*
* @param mixed $data
* post data from the entry form
* @param integer $status
* the status code resultant from processing the data.
* @param string $message
* the place to set any generated error message. any previous value for
* this variable will be overwritten.
* @param boolean $simulate (optional)
* true if this will tell the CF's to simulate data creation, false
* otherwise. this defaults to false. this is important if clients
* will be deleting or adding data outside of the main entry object
* commit function.
* @param mixed $entry_id (optional)
* the current entry. defaults to null.
* @return array
* the processed field data.
*/
public function processRawFieldData($data, &$status, &$message = null, $simulate = false, $entry_id = null)
{
$status = self::__OK__;
return array(
'value' => $data,
);
}
/**
* Returns the keywords that this field supports for filtering. Note
* that no filter will do a simple 'straight' match on the value.
*
* @since Symphony 2.6.0
* @return array
*/
public function fetchFilterableOperators()
{
return array(
array(
'title' => 'is',
'filter' => ' ',
'help' => __('Find values that are an exact match for the given string.')
),
array(
'filter' => 'sql: NOT NULL',
'title' => 'is not empty',
'help' => __('Find entries with a non-empty value.')
),
array(
'filter' => 'sql: NULL',
'title' => 'is empty',
'help' => __('Find entries with an empty value.')
),
array(
'title' => 'contains',
'filter' => 'regexp: ',
'help' => __('Find values that match the given <a href="%s">MySQL regular expressions</a>.', array(
'https://dev.mysql.com/doc/mysql/en/regexp.html'
))
),
array(
'title' => 'does not contain',
'filter' => 'not-regexp: ',
'help' => __('Find values that do not match the given <a href="%s">MySQL regular expressions</a>.', array(
'https://dev.mysql.com/doc/mysql/en/regexp.html'
))
),
);
}
/**
* Returns the types of filter suggestion this field supports.
* The array may contain the following values:
*
* - `entry` for searching entries in the current section
* - `association` for searching entries in associated sections
* - `static` for searching static values
* - `date` for searching in a calendar
* - `parameters` for searching in parameters
*
* If the date type is set, only the calendar will be shown in the suggestion dropdown.
*
* @since Symphony 2.6.0
* @return array
*/
public function fetchSuggestionTypes()
{
return array('entry');
}
/**
* Display the default data source filter panel.
*
* @param XMLElement $wrapper
* the input XMLElement to which the display of this will be appended.
* @param mixed $data (optional)
* the input data. this defaults to null.
* @param null $errors
* the input error collection. this defaults to null.
* @param string $fieldnamePrefix
* the prefix to apply to the display of this.
* @param string $fieldnamePostfix
* the suffix to apply to the display of this.
* @throws InvalidArgumentException
*/
public function displayDatasourceFilterPanel(XMLElement &$wrapper, $data = null, $errors = null, $fieldnamePrefix = null, $fieldnamePostfix = null)
{
$wrapper->appendChild(new XMLElement('header', '<h4>' . $this->get('label') . '</h4> <span>' . $this->name() . '</span>', array(
'data-name' => $this->get('label') . ' (' . $this->name() . ')'
)));
$label = Widget::Label(__('Value'));
$input = Widget::Input('fields[filter]'.($fieldnamePrefix ? '['.$fieldnamePrefix.']' : '').'['.$this->get('id').']'.($fieldnamePostfix ? '['.$fieldnamePostfix.']' : ''), ($data ? General::sanitize($data) : null));
$input->setAttribute('autocomplete', 'off');
$input->setAttribute('data-search-types', 'parameters');
$input->setAttribute('data-trigger', '{$');
$label->appendChild($input);
$wrapper->appendChild($label);
$this->displayFilteringOptions($wrapper);
}
/**
* Inserts tags at the bottom of the filter panel
*
* @since Symphony 2.6.0
* @param XMLElement $wrapper
*/
public function displayFilteringOptions(XMLElement &$wrapper)
{
// Add filter tags
$filterTags = new XMLElement('ul');
$filterTags->setAttribute('class', 'tags singular');
$filterTags->setAttribute('data-interactive', 'data-interactive');
$filters = $this->fetchFilterableOperators();
foreach ($filters as $value) {
$item = new XMLElement('li', $value['title']);
$item->setAttribute('data-value', $value['filter']);
if (isset($value['help'])) {
$item->setAttribute('data-help', General::sanitize($value['help']));
}
$filterTags->appendChild($item);
}
$wrapper->appendChild($filterTags);
$help = new XMLElement('p');
$help->setAttribute('class', 'help');
$first = array_shift($filters);
$help->setValue($first['help']);
$wrapper->appendChild($help);
}
/**
* Default accessor for the includable elements of this field. This array
* will populate the `Datasource` included elements. Fields that have
* different modes will override this and add new items to the array.
* The Symphony convention is element_name : mode. Modes allow Fields to
* output different XML in datasources.
*
* @return array
* the array of includable elements from this field.
*/
public function fetchIncludableElements()
{
return array($this->get('element_name'));
}
/**
* Test whether the input string is a regular expression, by searching
* for the prefix of `regexp:` or `not-regexp:` in the given `$string`.
*
* @deprecated @since Symphony 3.0.0
* Use EntryQueryFieldAdapter::isFilterRegex() instead
* @param string $string
* The string to test.
* @return boolean
* true if the string is prefixed with `regexp:` or `not-regexp:`, false otherwise.
*/
protected static function isFilterRegex($string)
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::isFilterRegex()',
'EntryQueryFieldAdapter::isFilterRegex()'
);
}
if (preg_match('/^regexp:/i', $string) || preg_match('/^not-?regexp:/i', $string)) {
return true;
}
}
/**
* Builds a basic REGEXP statement given a `$filter`. This function supports
* `regexp:` or `not-regexp:`. Users should keep in mind this function
* uses MySQL patterns, not the usual PHP patterns, the syntax between these
* flavours differs at times.
*
* @since Symphony 2.3
* @deprecated @since Symphony 3.0.0
* Use EntryQueryFieldAdapter::createFilterRegexp() instead
* @link https://dev.mysql.com/doc/refman/en/regexp.html
* @param string $filter
* The full filter, eg. `regexp: ^[a-d]`
* @param array $columns
* The array of columns that need the given `$filter` applied to. The conditions
* will be added using `OR` when using `regexp:` but they will be added using `AND`
* when using `not-regexp:`
* @param string $joins
* A string containing any table joins for the current SQL fragment. By default
* Datasources will always join to the `tbl_entries` table, which has an alias of
* `e`. This parameter is passed by reference.
* @param string $where
* A string containing the WHERE conditions for the current SQL fragment. This
* is passed by reference and is expected to be used to add additional conditions
* specific to this field
*/
public function buildRegexSQL($filter, array $columns, &$joins, &$where)
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::buildRegexSQL()',
'EntryQueryFieldAdapter::createFilterRegexp()'
);
}
$this->_key++;
$field_id = $this->get('id');
$filter = $this->cleanValue($filter);
$op = '';
if (preg_match('/^regexp:\s*/i', $filter)) {
$pattern = preg_replace('/^regexp:\s*/i', null, $filter);
$regex = 'REGEXP';
$op = 'OR';
} elseif (preg_match('/^not-?regexp:\s*/i', $filter)) {
$pattern = preg_replace('/^not-?regexp:\s*/i', null, $filter);
$regex = 'NOT REGEXP';
$op = 'AND';
} else {
throw new Exception("Filter `$filter` is not a Regexp filter");
}
if (strlen($pattern) == 0) {
return;
}
$joins .= "
LEFT JOIN
`tbl_entries_data_{$field_id}` AS t{$field_id}_{$this->_key}
ON (e.id = t{$field_id}_{$this->_key}.entry_id)
";
$where .= "AND ( ";
foreach ($columns as $key => $col) {
$modifier = ($key === 0) ? '' : $op;
$where .= "
{$modifier} t{$field_id}_{$this->_key}.{$col} {$regex} '{$pattern}'
";
}
$where .= ")";
}
/**
* Test whether the input string is a NULL/NOT NULL SQL clause, by searching
* for the prefix of `sql:` in the given `$string`, followed by `(NOT )? NULL`
*
* @since Symphony 2.7.0
* @deprecated @since Symphony 3.0.0
* Use EntryQueryFieldAdapter::isFilterSQL() instead
* @param string $string
* The string to test.
* @return boolean
* true if the string is prefixed with `sql:`, false otherwise.
*/
protected static function isFilterSQL($string)
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::isFilterSQL()',
'EntryQueryFieldAdapter::isFilterSQL()'
);
}
if (preg_match('/^sql:\s*(NOT )?NULL$/i', $string)) {
return true;
}
}
/**
* Builds a basic NULL/NOT NULL SQL statement given a `$filter`.
* This function supports `sql: NULL` or `sql: NOT NULL`.
*
* @since Symphony 2.7.0
* @deprecated @since Symphony 3.0.0
* Use EntryQueryFieldAdapter::createFilterSQL() instead
* @link https://dev.mysql.com/doc/refman/en/regexp.html
* @param string $filter
* The full filter, eg. `sql: NULL`
* @param array $columns
* The array of columns that need the given `$filter` applied to. The conditions
* will be added using `OR`.
* @param string $joins
* A string containing any table joins for the current SQL fragment. By default
* Datasources will always join to the `tbl_entries` table, which has an alias of
* `e`. This parameter is passed by reference.
* @param string $where
* A string containing the WHERE conditions for the current SQL fragment. This
* is passed by reference and is expected to be used to add additional conditions
* specific to this field
*/
public function buildFilterSQL($filter, array $columns, &$joins, &$where)
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::buildFilterSQL()',
'EntryQueryFieldAdapter::createFilterSQL()'
);
}
$this->_key++;
$field_id = $this->get('id');
$filter = $this->cleanValue($filter);
$pattern = '';
if (preg_match('/^sql:\s*NOT NULL$/i', $filter)) {
$pattern = 'NOT NULL';
} elseif (preg_match('/^sql:\s*NULL$/i', $filter)) {
$pattern = 'NULL';
} else {
// No match, return
return;
}
$joins .= "
LEFT JOIN
`tbl_entries_data_{$field_id}` AS t{$field_id}_{$this->_key}
ON (e.id = t{$field_id}_{$this->_key}.entry_id)
";
$where .= "AND ( ";
foreach ($columns as $key => $col) {
$modifier = ($key === 0) ? '' : 'OR';
$where .= "
{$modifier} t{$field_id}_{$this->_key}.{$col} IS {$pattern}
";
}
$where .= ")";
}
/**
* Construct the SQL statement fragments to use to retrieve the data of this
* field when utilized as a data source.
*
* @deprecated @since Symphony 3.0.0
* Use EntryQueryFieldAdapter::filter() instead
* @param array $data
* An array of the data that contains the values for the filter as specified
* in the datasource editor. The value that is entered in the datasource editor
* is made into an array by using + or , to separate the filter.
* @param string $joins
* A string containing any table joins for the current SQL fragment. By default
* Datasources will always join to the `tbl_entries` table, which has an alias of
* `e`. This parameter is passed by reference.
* @param string $where
* A string containing the WHERE conditions for the current SQL fragment. This
* is passed by reference and is expected to be used to add additional conditions
* specific to this field
* @param boolean $andOperation (optional)
* This parameter defines whether the `$data` provided should be treated as
* AND or OR conditions. This parameter will be set to true if $data used a
* + to separate the values, otherwise it will be false. It is false by default.
* @return boolean
* true if the construction of the SQL was successful, false otherwise.
*/
public function buildDSRetrievalSQL($data, &$joins, &$where, $andOperation = false)
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::buildDSRetrievalSQL()',
'EntryQueryFieldAdapter::filter()'
);
}
$field_id = $this->get('id');
// REGEX filtering is a special case, and will only work on the first item
// in the array. You cannot specify multiple filters when REGEX is involved.
if (self::isFilterRegex($data[0])) {
$this->buildRegexSQL($data[0], array('value'), $joins, $where);
// SQL filtering: allows for NULL/NOT NULL statements
} elseif (self::isFilterSQL($data[0])) {
$this->buildFilterSQL($data[0], array('value'), $joins, $where);
// AND operation, iterates over `$data` and uses a new JOIN for
// every item.
} elseif ($andOperation) {
foreach ($data as $value) {
$this->_key++;
$value = $this->cleanValue($value);
$joins .= "
LEFT JOIN
`tbl_entries_data_{$field_id}` AS t{$field_id}_{$this->_key}
ON (e.id = t{$field_id}_{$this->_key}.entry_id)
";
$where .= "
AND t{$field_id}_{$this->_key}.value = '{$value}'
";
}
// Default logic, this will use a single JOIN statement and collapse
// `$data` into a string to be used in conjunction with IN
} else {
foreach ($data as &$value) {
$value = $this->cleanValue($value);
}
$this->_key++;
$data = implode("', '", $data);
$joins .= "
LEFT JOIN
`tbl_entries_data_{$field_id}` AS t{$field_id}_{$this->_key}
ON (e.id = t{$field_id}_{$this->_key}.entry_id)
";
$where .= "
AND t{$field_id}_{$this->_key}.value IN ('{$data}')
";
}
return true;
}
/**
* Determine if the requested $order is random or not.
*
* @since Symphony 2.7.0
* @deprecated @since Symphony 3.0.0
* Use EntryQueryFieldAdapter::isRandomOrder() instead
* @param string $order
* the sorting direction.
* @return boolean
* true if the $order is either 'rand' or 'random'
*/
protected function isRandomOrder($order)
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::isRandomOrder()',
'EntryQueryFieldAdapter::isRandomOrder()'
);
}
return in_array(strtolower($order), array('random', 'rand'));
}
/**
* Build the SQL command to append to the default query to enable
* sorting of this field. By default this will sort the results by
* the entry id in ascending order.
*
* Extension developers should always implement both `buildSortingSQL()`
* and `buildSortingSelectSQL()`.
*
* @deprecated @since Symphony 3.0.0
* Use EntryQueryFieldAdapter::sort() instead
* @uses Field::isRandomOrder()
* @see Field::buildSortingSelectSQL()
* @param string $joins
* the join element of the query to append the custom join sql to.
* @param string $where
* the where condition of the query to append to the existing where clause.
* @param string $sort
* the existing sort component of the sql query to append the custom
* sort sql code to.
* @param string $order (optional)
* an optional sorting direction. this defaults to ascending. if this
* is declared either 'random' or 'rand' then a random sort is applied.
*/
public function buildSortingSQL(&$joins, &$where, &$sort, $order = 'ASC')
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::buildSortingSQL()',
'EntryQueryFieldAdapter::sort()'
);
}
if ($this->isRandomOrder($order)) {
$sort = 'ORDER BY RAND()';
} else {
$joins .= "LEFT OUTER JOIN `tbl_entries_data_".$this->get('id')."` AS `ed` ON (`e`.`id` = `ed`.`entry_id`) ";
$sort = sprintf('ORDER BY `ed`.`value` %1$s, `e`.`id` %1$s', $order);
}
}
/**
* Build the needed SQL clause command to make `buildSortingSQL()` work on
* MySQL 5.7 in strict mode, which requires all columns in the ORDER BY
* clause to be included in the SELECT's projection.
*
* If no new projection is needed (like if the order is made via a sub-query),
* simply return null.
*
* For backward compatibility, this method checks if the sort expression
* contains `ed`.`value`. This check will be removed in Symphony 3.0.0.
*
* Extension developers should make their Fields implement
* `buildSortingSelectSQL()` when overriding `buildSortingSQL()`.
*
* @deprecated @since Symphony 3.0.0
* Use EntryQueryFieldAdapter::sort() instead
* @since Symphony 2.7.0
* @uses Field::isRandomOrder()
* @see Field::buildSortingSQL()
* @param string $sort
* the existing sort component of the sql query, after it has been passed
* to `buildSortingSQL()`
* @param string $order (optional)
* an optional sorting direction. this defaults to ascending. Should be the
* same value that was passed to `buildSortingSQL()`
* @return string
* an optional select clause to append to the generated SQL query.
* This is needed when sorting on a column that is not part of the projection.
*/
public function buildSortingSelectSQL($sort, $order = 'ASC')
{
if (Symphony::Log()) {
Symphony::Log()->pushDeprecateWarningToLog(
get_called_class() . '::buildSortingSelectSQL()',
'EntryQueryFieldAdapter::sort()'
);
}
if ($this->isRandomOrder($order)) {
return null;
}
// @deprecated This check should be removed in Symphony 3.0.0
if (strpos($sort, '`ed`.`value`') === false) {
return null;
}
return '`ed`.`value`';
}
/**
* Default implementation of record grouping. This default implementation
* will throw an `Exception`. Thus, clients must overload this method
* for grouping to be successful.
*
* @throws Exception
* @param array $records
* the records to group.
*/
public function groupRecords($records)
{
throw new Exception(
__('Data source output grouping is not supported by the %s field', array('<code>' . $this->get('label') . '</code>'))
);
}
/**
* Function to format this field if it chosen in a data source to be
* output as a parameter in the XML.
*
* Since Symphony 2.5.0, it will defaults to `prepareReadableValue` return value.
*
* @param array $data
* The data for this field from it's `tbl_entry_data_{id}` table
* @param integer $entry_id
* The optional id of this field entry instance
* @return string|array
* The formatted value to be used as the parameter. Note that this can be
* an array or a string. When returning multiple values use array, otherwise
* use string.
*/
public function getParameterPoolValue(array $data, $entry_id = null)
{
return $this->prepareReadableValue($data, $entry_id);
}
/**
* Append the formatted XML output of this field as utilized as a data source.
*
* Since Symphony 2.5.0, it will defaults to `prepareReadableValue` return value.
*
* @param XMLElement $wrapper
* the XML element to append the XML representation of this to.
* @param array $data
* the current set of values for this field. the values are structured as
* for displayPublishPanel.
* @param boolean $encode (optional)
* flag as to whether this should be html encoded prior to output. this
* defaults to false.
* @param string $mode
* A field can provide ways to output this field's data. For instance a mode
* could be 'items' or 'full' and then the function would display the data
* in a different way depending on what was selected in the datasource
* included elements.
* @param integer $entry_id (optional)
* the identifier of this field entry instance. defaults to null.
*/
public function appendFormattedElement(XMLElement &$wrapper, $data, $encode = false, $mode = null, $entry_id = null)
{
$wrapper->appendChild(new XMLElement($this->get('element_name'), ($encode ?
General::sanitize($this->prepareReadableValue($data, $entry_id)) :
$this->prepareReadableValue($data, $entry_id))));
}
/**
* The default method for constructing the example form markup containing this
* field when utilized as part of an event. This displays in the event documentation
* and serves as a basic guide for how markup should be constructed on the
* `Frontend` to save this field
*
* @throws InvalidArgumentException
* @return XMLElement
* a label widget containing the formatted field element name of this.
*/
public function getExampleFormMarkup()
{
$label = Widget::Label($this->get('label'));
$label->appendChild(Widget::Input('fields['.$this->get('element_name').']'));
return $label;
}
/**
* Commit the settings of this field from the section editor to
* create an instance of this field in a section.
*
* @return boolean
* true if the commit was successful, false otherwise.
*/
public function commit()
{
$fields = array();
$fields['label'] = General::sanitize($this->get('label'));
$fields['element_name'] = ($this->get('element_name') ? $this->get('element_name') : Lang::createHandle($this->get('label')));
$fields['parent_section'] = $this->get('parent_section');
$fields['location'] = $this->get('location');
$fields['required'] = $this->get('required');
$fields['type'] = $this->_handle;
$fields['show_column'] = $this->get('show_column');
$fields['sortorder'] = (string)$this->get('sortorder');
if ($id = $this->get('id')) {
return FieldManager::edit($id, $fields);
} elseif ($id = FieldManager::add($fields)) {
$this->set('id', $id);
if ($this->requiresTable()) {
return $this->createTable();
}
return true;
}
return false;
}
/**
* The default field table construction method. This constructs the bare
* minimum set of columns for a valid field table. Subclasses are expected
* to overload this method to create a table structure that contains
* additional columns to store the specific data created by the field.
*
* @throws DatabaseException
* @see Field::requiresTable()
* @return boolean
*/
public function createTable()
{
return Symphony::Database()
->create('tbl_entries_data_' . General::intval($this->get('id')))
->ifNotExists()
->fields([
'id' => [
'type' => 'int(11)',
'auto' => true,
],
'entry_id' => 'int(11)',
'value' => [
'type' => 'varchar(255)',
'null' => true,
],
])
->keys([
'id' => 'primary',
'entry_id' => 'key',
'value' => 'key',
])
->execute()
->success();
}
/**
* Tells Symphony that this field needs a table in order to store
* data for each of its entries. Used when adding/deleting this field in a section
* or entries are edited/added, data as a performance optimization.
* It defaults to true, which force table creation.
*
* Developers are encouraged to update their null create table implementation
* with this method.
*
* @since Symphony 2.7.0
* @see Field::createTable()
* @return boolean
* true if Symphony should call `createTable()`
*/
public function requiresTable()
{
return true;
}
/**
* Checks that we are working with a valid field handle and
* that the setting table exists.
*
* @since Symphony 2.7.0
* @return boolean
* true if the table exists, false otherwise
*/
public function tableExists()
{
if (!$this->_handle) {
return false;
}
return Symphony::Database()->tableExists('tbl_fields_' . $this->_handle);
}
/**
* Checks that we are working with a valid field handle and field id, and
* checks that the field record exists in the settings table.
*
* @since Symphony 2.7.1 It does check if the settings table only contains
* default columns and assume those fields do not require a record in the settings table.
* When this situation is detected the field is considered as valid even if no records were
* found in the settings table.
*
* @since Symphony 2.7.0
* @see Field::tableExists()
* @return boolean
* true if the field id exists in the table, false otherwise
*/
public function exists()
{
if (!$this->get('id') || !$this->_handle) {
return false;
}
$row = Symphony::Database()
->select(['id'])
->from('tbl_fields_' . $this->_handle)
->where(['field_id' => $this->get('id')])
->execute()
->rows();
if (empty($row)) {
// Some fields do not create any records in their settings table because they do not
// implement a proper `Field::commit()` method.
// The base implementation of the commit function only deals with the "core"
// `tbl_fields` table.
// The problem with this approach is that it can lead to data corruption when
// saving a field that got deleted by another user.
// The only way a field can live without a commit method is if it does not store any
// settings at all.
// But current version of Symphony assume that the `tbl_fields_$handle` table exists
// with at least a `id` and `field_id` column, so field are required to at least create
// the table to make their field work without SQL errors from the core.
$columns = Symphony::Database()
->describe('tbl_fields_' . $this->_handle)
->execute()
->column('Field');
// The table only has the two required columns, tolerate the missing record
$isDefault = count($columns) === 2 &&
in_array('id', $columns) &&
in_array('field_id', $columns);
if ($isDefault) {
Symphony::Log()->pushDeprecateWarningToLog($this->_handle, get_class($this), array(
'message-format' => __('The field `%1$s` does not create settings records in the `tbl_fields_%1$s`.'),
'alternative-format' => __('Please implement the commit function in class `%s`.'),
'removal-format' => __('The compatibility check will will be removed in Symphony %s.'),
'removal-version' => '4.0.0',
));
}
return $isDefault;
}
return true;
}
/**
* Remove the entry data of this field from the database.
*
* @param integer|array $entry_id
* the ID of the entry, or an array of entry ID's to delete.
* @param array $data (optional)
* The entry data provided for fields to do additional cleanup
* This is an optional argument and defaults to null.
* @throws DatabaseException
* @return boolean
* Returns true after the cleanup has been completed
*/
public function entryDataCleanup($entry_id, $data = null)
{
if (!is_array($entry_id)) {
$entry_id = [$entry_id];
}
return Symphony::Database()
->delete('tbl_entries_data_' . $this->get('id'))
->where(['entry_id' => ['in' => $entry_id]])
->execute()
->success();
}
/**
* Accessor to the associated entry search value for this field
* instance. This default implementation simply returns `$data`
*
* @param array $data
* the data from which to construct the associated search entry value, this is usually
* Entry with the `$parent_entry_id` value's data.
* @param integer $field_id (optional)
* the ID of the field that is the parent in the relationship
* @param integer $parent_entry_id (optional)
* the ID of the entry from the parent section in the relationship
* @return array|string
* Defaults to returning `$data`, but overriding implementation should return
* a string
*/
public function fetchAssociatedEntrySearchValue($data, $field_id = null, $parent_entry_id = null)
{
return $data;
}
/**
* Fetch the count of the associated entries given a `$value`.
*
* @see toolkit.Field#fetchAssociatedEntrySearchValue()
* @param mixed $value
* the value to find the associated entry count for, this usually comes from
* the `fetchAssociatedEntrySearchValue` function.
* @return void|integer
* this default implementation returns void. overriding implementations should
* return an integer.
*/
public function fetchAssociatedEntryCount($value)
{
}
/**
* Find related entries from a linking field's data table. Default implementation uses
* column names `entry_id` and `relation_id` as with the Select Box Link
*
* @since Symphony 2.5.0
*
* @param integer $entry_id
* @param integer $parent_field_id
* @return array
*/
public function findRelatedEntries($entry_id, $parent_field_id)
{
try {
$ids = Symphony::Database()
->select(['entry_id'])
->from('tbl_entries_data_' . $this->get('id'))
->where(['relation_id' => General::intval($entry_id)])
->where(['entry_id' => ['!=' => null]])
->execute()
->column('entry_id');
} catch (Exception $e) {
return [];
}
return $ids;
}
/**
* Find related entries for the current field. Default implementation uses
* column names `entry_id` and `relation_id` as with the Select Box Link
*
* @since Symphony 2.5.0
*
* @param integer $parent_field_id
* @param integer $entry_id
* @return array
*/
public function findParentRelatedEntries($parent_field_id, $entry_id)
{
try {
$ids = Symphony::Database()
->select(['relation_id'])
->from('tbl_entries_data_' . $this->get('id'))
->where(['entry_id' => General::intval($entry_id)])
->where(['relation_id' => ['!=' => null]])
->execute()
->column('relation_id');
} catch (Exception $e) {
return [];
}
return $ids;
}
/**
* Converts all $values into proper ones to use with prepopulate and filtering.
* The default implementation simply returns the $values array.
*
* @param array $values
* The original filter values
* @return array
* The proper filter values to use in query strings
*/
public function convertValuesToFilters(array $values)
{
return $values;
}
}