diff -u b/serial.inc b/serial.inc --- b/serial.inc +++ b/serial.inc @@ -57,7 +57,7 @@ $query->join('field_config_instance', 'i', '(f.field_name = i.field_name)'); - $query->condition('f.type', 'serial'); + $query->condition('f.type', SERIAL_FIELD_TYPE); $query->condition('i.entity_type', $entity_type); $query->condition('i.bundle', $bundle_new); @@ -99,7 +99,9 @@ * The name of the assistant table of the specified field. */ function _serial_get_table_name($entity_type, $bundle, $field_name) { - return db_escape_table("serial_{$entity_type}_{$bundle}_{$field_name}"); + // Remember about max length of MySQL tables - 64 symbols. + // @todo Think about improvement for this. + return db_escape_table('serial_' . md5("{$entity_type}_{$bundle}_{$field_name}")); } /** @@ -112,7 +114,7 @@ return array( 'fields' => array( 'sid' => array( - 'type' => 'serial', + 'type' => SERIAL_FIELD_TYPE, 'not null' => TRUE, 'unsigned' => TRUE, 'description' => 'The atomic serial field.', @@ -146,28 +148,39 @@ * * @return int * the unique serial value number. + * + * @throws \Exception */ function _serial_generate_value($entity_type, $bundle, $field_name, $delete = TRUE) { - // Get the name of the relevant table. - $table = _serial_get_table_name($entity_type, $bundle, $field_name); - // Insert a temporary record to get a new unique serial value. - $uniqid = uniqid('', TRUE); - $sid = db_insert($table) - ->fields(array('uniqid' => $uniqid)) - ->execute(); - - // If there's a reason why it's come back undefined, reset it. - $sid = isset($sid) ? $sid : 0; - - // Delete the temporary record. - if ($delete && ($sid % 10) == 0) { - db_delete($table) - ->condition('uniqid', $uniqid) + $transaction = db_transaction(); + + try { + // Get the name of the relevant table. + $table = _serial_get_table_name($entity_type, $bundle, $field_name); + // Insert a temporary record to get a new unique serial value. + $uniqid = uniqid('', TRUE); + $sid = db_insert($table) + ->fields(array('uniqid' => $uniqid)) ->execute(); - } - // Return the new unique serial value. - return $sid; + // If there's a reason why it's come back undefined, reset it. + $sid = isset($sid) ? $sid : 0; + + // Delete the temporary record. + if ($delete && ($sid % 10) == 0) { + db_delete($table) + ->condition('uniqid', $uniqid) + ->execute(); + } + + // Return the new unique serial value. + return $sid; + } + catch (Exception $e) { + $transaction->rollback(); + watchdog_exception('serial', $e); + throw $e; + } } /** @@ -230,7 +243,7 @@ return $query ->fields('i', array('entity_type', 'bundle', 'field_name')) - ->condition('f.type', 'serial') + ->condition('f.type', SERIAL_FIELD_TYPE) ->condition('i.deleted', 0) ->execute() ->fetchAll(); diff -u b/serial.info b/serial.info --- b/serial.info +++ b/serial.info @@ -2,5 +2,8 @@ description = Defines atomic auto increment (serial) field type. -package = Fields +package = Field core = 7.x +files[] = tests/serial.test +files[] = tests/serial.fields.inc + dependencies[] = field diff -u b/serial.install b/serial.install --- b/serial.install +++ b/serial.install @@ -7,7 +7,7 @@ /** * Implements hook_field_schema(). */ -function serial_field_schema($field) { +function serial_field_schema(array $field) { $columns = array(); switch ($field['type']) { @@ -85,0 +86,13 @@ + +/** + * Reorganize table names to prevent collisions with long names. + */ +function serial_update_7132() { + module_load_include('inc', 'serial'); + + foreach (db_find_tables('serial_%') as $table) { + // Explode by underscores and match old format. + list(, $entity_type, $bundle, $field_name) = explode('_', $table); + db_rename_table($table, _serial_get_table_name($entity_type, $bundle, $field_name)); + } +} diff -u b/serial.module b/serial.module --- b/serial.module +++ b/serial.module @@ -4,20 +4,45 @@ * The Serial module main file. */ +define('SERIAL_FIELD_TYPE', 'serial'); + /** * Implements hook_field_info(). */ function serial_field_info() { return array( - 'serial' => array( + SERIAL_FIELD_TYPE => array( 'label' => t('Serial'), 'description' => t('Auto increment serial field type.'), // The "property_type" should be defined for accessing the // field by entity metadata wrapper. - 'property_type' => 'serial', - 'default_widget' => 'serial', - 'default_formatter' => 'serial_formatter_default', 'property_type' => 'integer', + 'default_widget' => 'serial_widget_default', + 'default_formatter' => 'serial_formatter_default', + ), + ); +} + +/** + * Implements hook_field_widget_info(). + */ +function serial_field_widget_info() { + return array( + 'serial_widget_default' => array( + 'label' => t('Hidden (Automatic)'), + 'field types' => array(SERIAL_FIELD_TYPE), + ), + ); +} + +/** + * Implements hook_field_formatter_info(). + */ +function serial_field_formatter_info() { + return array( + 'serial_formatter_default' => array( + 'label' => t('Default'), + 'field types' => array(SERIAL_FIELD_TYPE), ), ); } @@ -28,7 +53,7 @@ function serial_field_create_instance(array $instance) { $field = field_read_field($instance['field_name']); - if ('serial' == $field['type']) { + if (SERIAL_FIELD_TYPE == $field['type']) { // Create the assistant table: module_load_include('inc', 'serial'); _serial_create_table($field, $instance); @@ -50,7 +75,7 @@ function serial_field_delete_instance(array $instance) { $field = field_read_field($instance['field_name']); - if ('serial' == $field['type']) { + if (SERIAL_FIELD_TYPE == $field['type']) { // Drop the assistant table. module_load_include('inc', 'serial'); _serial_drop_table($field, $instance); @@ -58,30 +83,15 @@ } /** - * Implements hook_form_alter(). - */ -function serial_form_alter(array &$form, array &$form_state, $form_id) { - if ('field_ui_field_settings_form' == $form_id && 'serial' == $form['field']['type']['#value']) { - drupal_set_message(t('Serial field %field has been created.', array( - '%field' => $form['field']['field_name']['#value'], - ))); - - drupal_goto("admin/structure/types/manage/{$form['#bundle']}/fields"); - } -} - -/** * Implements hook_field_presave(). */ function serial_field_presave($entity_type, $entity, array $field, array $instance, $langcode, array &$items) { - if (empty($items)) { - module_load_include('inc', 'serial'); + module_load_include('inc', 'serial'); - $items = array( - array( - 'value' => _serial_generate_value($entity_type, $instance['bundle'], $field['field_name']), - ), - ); + // Remember that here can be only one item + // but loop created to make code nicer. + foreach ($items as $delta => $item) { + $items[$delta]['value'] = _serial_generate_value($entity_type, $instance['bundle'], $field['field_name']); } } @@ -96,7 +106,7 @@ /** * Implements hook_node_presave(). */ -function serial_node_presave($node) { +function serial_node_presave(\stdClass $node) { if (module_exists('auto_nodetitle') && auto_nodetitle_is_needed($node)) { auto_nodetitle_set_title($node); } @@ -113,21 +123,9 @@ } /** - * Implements hook_field_formatter_info(). - */ -function serial_field_formatter_info() { - return array( - 'serial_formatter_default' => array( - 'label' => t('Default'), - 'field types' => array('serial'), - ), - ); -} - -/** * Implements hook_field_formatter_view(). */ -function serial_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, array $items, $display) { +function serial_field_formatter_view($entity_type, $entity, array $field, array $instance, $langcode, array $items, array $display) { $element = array(); // Define the field contents for the single default formatter. @@ -164,34 +162,29 @@ } /** - * Implements hook_field_widget_info(). + * Implements hook_field_widget_form(). */ -function serial_field_widget_info() { - return array( - 'serial' => array( - 'label' => t('Hidden (Automatic)'), - 'field types' => array('serial'), - ), - ); -} +function serial_field_widget_form( + array &$form, + array &$form_state, + array $field, + array $instance, + $langcode, + array $items, + $delta, + array $element +) { + $element['#type'] = 'hidden'; -/** - * Implements hook_field_widget(). - */ -function serial_field_widget(array &$form, array &$form_state, array $field, array $instance, array $items, $delta = 0) { - return array( - 'value' => array( - '#type' => 'hidden', - '#default_value' => $items[$delta]['value'], - ), - ); -} + if (isset($items[$delta]['value'])) { + $element['#default_value'] = $items[$delta]['value']; + } + + return array('value' => $element); } /** * Implements hook_tokens(). - * - * Replace token for generic entity type. */ function serial_tokens($type, array $tokens, array $data = array(), array $options = array()) { $entity_info = entity_get_info($type); @@ -209,7 +202,7 @@ $field_name = str_replace('-', '_', $name); $field_info = field_info_field($field_name); - if ('serial' === $field_info['type']) { + if (SERIAL_FIELD_TYPE === $field_info['type']) { continue; } @@ -223,9 +216,12 @@ - if (empty($items)) { - continue; + if (FALSE !== $items) { + // Remember that here can be only one item + // but loop created to make code nicer. + foreach ($items as $delta => $item) { + $replacements[$original] = $item['value']; + } } - - $replacements[$original] = $items[0]['value']; } return $replacements; } +} only in patch2: unchanged: --- /dev/null +++ b/tests/serial.fields.inc @@ -0,0 +1,191 @@ +fields = $fields; + } + + /** + * Create fields. + * + * @return self + * Object instance. + * + * @throws \FieldException + * When cannot create a field. + */ + public function create() { + foreach ($this->fields as $name => $data) { + if (!db_table_exists("field_data_$name")) { + field_create_field($data + array( + 'default' => '', + 'not null' => TRUE, + 'field_name' => $name, + )); + } + } + + return $this; + } + + /** + * Completely delete fields. + * + * This function deletes tables: "field_data_NAME" and "field_revision_NAME" + * and entries in "field_config" and "field_config_instances". + * + * @return self + * Object instance. + */ + public function delete() { + foreach (array_keys($this->fields) as $name) { + // Delete tables. + foreach (array('data', 'revision') as $table_type) { + $table = "field_{$table_type}_{$name}"; + + if (db_table_exists($table)) { + db_drop_table($table); + } + } + + // Delete entries. + foreach (array('config', 'config_instance') as $table_type) { + db_delete("field_$table_type") + ->condition('field_name', $name) + ->execute(); + } + } + + return $this; + } + + /** + * Attach existing fields into entity. + * + * @param string $entity_type + * Entity machine name. + * @param string $bundle_name + * Entity bundle name. + * + * @return self + * Object instance. + * + * @throws \FieldException + * When instance cannot be created. + */ + public function attach($entity_type, $bundle_name) { + $attached_fields = field_info_instances($entity_type, $bundle_name); + + foreach ($this->fields as $field_name => $data) { + if (empty($attached_fields[$field_name]) && field_info_field($field_name)) { + // Provide a possibility to specify field weight, depending on + // another one. + // + // @code + // $fields = array( + // 'field_title' => array( + // 'type' => 'text', + // 'label' => 'Title', + // 'widget' => array( + // 'weight' => 10, + // ), + // ), + // 'field_description' => array( + // 'type' => 'text', + // 'label' => 'Description', + // 'widget' => array( + // // Weight of this field will be "9". + // 'weight' => array('field_title', -1), + // ), + // ), + // ); + // @endcode + if (isset($data['widget']['weight']) && is_array($data['widget']['weight'])) { + list($dependent, $calc) = $data['widget']['weight']; + + $dependent = field_info_instance($entity_type, $dependent, $bundle_name); + + if (!empty($dependent)) { + $data['widget']['weight'] = $dependent['widget']['weight'] + $calc; + } + } + + field_create_instance($data + array( + 'bundle' => $bundle_name, + 'field_name' => $field_name, + 'entity_type' => $entity_type, + )); + } + } + + return $this; + } + + /** + * Get field instances. + * + * @return array[] + * Field instances. + */ + public function &getInstances() { + if (empty($this->instances)) { + $query = db_select('field_config_instance', 'fci') + ->fields('fci', array('field_name', 'data')) + ->condition('field_name', array_keys($this->fields)) + ->execute() + ->fetchAllKeyed(); + + $this->instances = array_map('unserialize', $query); + } + + return $this->instances; + } + + /** + * Field definitions getter. + * + * @return array[] + * Field definitions. + */ + public function getFields() { + return $this->fields; + } + + /** + * Save field instances. + * + * @return self + * Object instance. + * + * @throws \Exception + * @throws \InvalidMergeQueryException + */ + public function saveInstances() { + foreach ($this->instances as $field_name => $data) { + db_merge('field_config_instance') + ->fields(array('data' => serialize($data))) + ->condition('field_name', $field_name) + ->execute(); + } + + return $this; + } + +} only in patch2: unchanged: --- /dev/null +++ b/tests/serial.test @@ -0,0 +1,128 @@ + 'Serial Field', + 'group' => 'Field', + 'description' => 'Testing serial field functionality.', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp('serial', 'comment'); + + $this->drupalCreateContentType(array('type' => static::CONTENT_TYPE)); + + $this->fields = new \SerialFields(array( + 'serial' => array( + 'type' => SERIAL_FIELD_TYPE, + 'label' => 'Serial', + 'settings' => array(), + ), + )); + + $this->fields + ->create() + // Attach serial field to content type and comments form. + ->attach('node', static::CONTENT_TYPE) + ->attach('comment', 'comment_node_' . static::CONTENT_TYPE); + + // Grant all known permissions for user. + $this->drupalLogin($this->drupalCreateUser(array_keys(module_invoke_all('permission')))); + } + + /** + * Create N nodes and attach N comments for the last. + * + * @param int $nodes + * Number of nodes for creation. + * @param int $comments + * Number of comments for creation. + */ + public function testSerial($nodes = 3, $comments = 6) { + for ($i = 0; $i < $nodes; $i++) { + // Open form for add new node. + $this->visit('node/add/' . str_replace('_', '-', static::CONTENT_TYPE)); + // Submit new node with filled title. + $this->drupalPost(NULL, array('title' => "Node $i"), t('Save')); + } + + // Go to editing of the last created node. + $this->visit("node/$nodes/edit"); + // Check that last created node number equal to serial ID. + $this->assertSerialField($nodes); + // Go to viewing of the last created node. + $this->visit("node/$nodes"); + + // Post comments for last created node. + for ($i = 0; $i < $comments; $i++) { + $this->drupalPost(NULL, array(self::fieldName('comment_body') => "Comment $i"), t('Save')); + } + + // Go to editing of the last created comment. + $this->visit("comment/$comments/edit"); + // Ensure the last-posted comment number equal to serial ID. + $this->assertSerialField($comments); + } + + /** + * Assert number with value of the serial field on the page. + * + * @param int $number + * The number for verification. + */ + private function assertSerialField($number) { + $this->assertFieldByXPath($this->constructFieldXpath('name', self::fieldName('serial')), $number); + } + + /** + * Visit path and assert response code. + * + * @param string $path + * Path to visit. + * @param int $code + * Expected response code. + */ + private function visit($path, $code = 200) { + $this->drupalGet($path); + $this->assertResponse($code); + } + + /** + * Convert Drupal field name into HTML. + * + * @param string $name + * Drupal field name. + * @param string $column + * Field column. + * + * @return string + * HTML input name. + */ + private static function fieldName($name, $column = 'value') { + return $name . '[' . LANGUAGE_NONE . '][0][' . $column . ']'; + } + +}