Skip to main content

Blog

On my quest to improve a client's Drupal site performance I considered installing the Alternative PHP Cache. It reduces the overhead of compiling the PHP sources into opcode on each request by caching the compiled code1. 2bits posted a very good case study about PHP opcode caches a while ago.

I have seen significant performance improvements from opcode caches in past Drupal projects. But every site is different. Usually the relative efficiency of an opcode cache correlates with the share the bootstrap process takes of total page rendering time. This can be easily measured with a profiling tool like Xdebug and a visualization software (Kcachegrind is an excellent free software product, for MacOS X there's MacCallGrind).

 

 

 

Visualization of Xdebug profiling results with MacCallGrind

After the first profiling run, it became clear that the bootstrap process only amounted to about 10% of total page rendering time. Since the full rendering period was an order of magnitude greater than the whole bootstrapping period, any performance improvement in the bootstrapping process would have no perceivable performance impact. Nevertheless I took some time to make some observations. I was especially curious about comparing APC running with apc.stat enabled and disabled.

For the purpose of this test I put the site on a Virtual Box machine running Debian Wheezy: PHP 5.3.10 with Xdebug 2.1.3, MySQL 5.1.58, PHP APC 3.1.2, Apache 2.2.22. Some initial test runs were made to establish the amount of memory APC needed to avoid cache resets, the actual experiment used a value of 128 MB for apc.shm_size. To warm up the APC cache two requests to the page were made before starting each test series. A test series consists of requesting the homepage with profiling enabled and noting the total execution time of drupal_bootstrap and the share it has of the total page rendering time — repeated 10 times.

 

 

 

Execution time of drupal_bootstrap with APC cache disabled

 

 

 

Execution time of drupal_bootstrap with APC cache enabled

The results show a reduction in execution time between 25% and 60% taking the standard deviation into account. Disabling apc.stat had no measurable effect.

1. PHP has been designed for adding dynamic content to web pages by embedding snippets of code (<?php ... ?>) in HTML markup. Still the most popular way of running PHP is by means of Apache with mod_php which is originally written for that use case. Each time a request comes to a page containing PHP that code is parsed and executed in fractions of a second. The growth of the PHP community and increasing complexity of problems being solved with PHP has led to the development of ever more complex software - like Drupal. If you look at the source of Drupal most of the files contain purely PHP with a little bit of HTML here and there, primarily in the themes. Now the cost of the parser loading and compiling the sources into byte code to be executed by the interpreter has become a challenge. An opcode cache like APC saves time because it remembers the compiled code in memory thereby reducing the overhead per request.

Social Simple buttons

This covers 90% of use cases, but what if we need to add a button for a new network?

Creating a Custom Social Simple Button

The Social Simple module already supports custom buttons, we just need to let the module know that we want to add one.

What we need to do is:

  • Create a class that implements SocialNetworkInterface.
  • Register this class in our services file.
  • Add the tag social_simple_network to our service.

For our example we are going to create a basic Mail button. We start by creating a custom module. Inside our module let's create a Mail php file inside of the src/SocialNetwork folder:

 

mkdir -p src/SocialNetwork cd src/SocialNetwork touch Mail.php

 

The next step is to create a class and implement the SocialNetworkInterface which interface has the following methods:

  • getShareLink: This is the most important method. It must return a rendered array which later Drupal will use to create the button.
  • getLabel: Here we will need to provide the name of our button. In our case Mail.
  • getId: The ID of the button. We can choose any ID here, we just need to make sure that it is unique. Let's use mail for our example.
  • getLinkAttributes: These attributes are going to be passed to the link. We can add custom parameters to the link in this part.

Our class looks like this:


namespace Drupal\social_simple\SocialNetwork; use Drupal\Core\Entity\EntityInterface; 

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url; 

/** * The Mail button. */
class Mail implements SocialNetworkInterface { 

use StringTranslationTrait; 

/** * The social network base share link. */
const MAIL = 'mailto:'; 

/** * {@inheritdoc} */
public function getId() {
  return 'mail';
} 

/** * {@inheritdoc} */
public function getLabel() {
  return $this->t('Mail');
} 

/** * {@inheritdoc} */
public function getShareLink($share_url, $title = '', EntityInterface $entity = NULL, array $additional_options = []) {
  $options = [
    'query' => [
      'body' => $share_url,
      'subject' => $title,
    ],
    'absolute' => TRUE,
    'external' => TRUE,
  ]; 

  if ($additional_options) {
    foreach ($additional_options as $id => $value) {
      $options['query'][$id] = $value;
    }
  }
  $url = Url::fromUri(self::MAIL, $options);
  $link = [
    'url' => $url,
    'title' => ['#markup' => '' . $this->getLabel() . ''],
    'attributes' => $this->getLinkAttributes($this->getLabel()),
  ]; return $link;
} 

/** * {@inheritdoc} */
public function getLinkAttributes($network_name) {
  $attributes = [ 'title' => $network_name, ];
  return $attributes;
  }
}

The next step is to let the social network know about our new button and we do this by adding this class as a service in our module.services.yml. If you are not familiar with this file, you can read the structure of a service file documentation..

Basically we need to add something like this:


services: 
  social_simple.mail: 
    class: Drupal\custom_module\SocialNetwork\Mail 
    tags: - { name: social_simple_network, priority: 0 }

Next, rebuild the cache. Now when we visit the social simple configuration we will see our new button there, ready to be used.

Social Simple Configuration page

The only thing that we need to pay extra attention to is that the Social Simple module will just search the services with the tag social_simple_network otherwise our class will not be found.

If you want to see how the whole thing is working, you can check this patch that I made as a part of a project: https://www.drupal.org/project/social_simple/issues/2899517. As a bonus, I made an initial integration with the Forward module.

We will send you very occasional dispatches from our perspective on various overlapping movements for cooperation, freedom and justice as workers and as passionate observers.

The Views module provides a flexible method for Drupal site builders to present data. On a recent project we needed to filter a view's result set in a way we could not achieve by means of the module's UI. How do you programmatically alter a view's result set before rendering? Let's see how to do it using the hooks provided by the module.

The need surfaced while working on the web site for MIT's Global Studies and Languages department, which uses Views to pull in data from a remote service and display it. The Views module provides a flexible method for Drupal site builders to present data. Most of the time you can configure your presentation needs through the UI using Views and Views-related contributed modules. Notwithstanding, sometimes you need to implement a specific requirement which is not available out of the box. Luckily, Views provides hooks to alter its behavior and results. Let’s see how to filter Views results before they are rendered.

Assume we have a website which aggregates book information from different sources. We store the book name, author, year of publication, and ISBN (International Standard Book Number). ISBNs are unique numerical book identifiers which can be 10 or 13 characters long. The last digit in either version is a verification number and the 13 character version has a 3-character prefix. The other numbers are the same. A book can have both versions. For example:

ISBN10:    1849511160
ISBN13: 9781849511162

In our example website, we only use one ISBN. If both versions are available, the 10-character version is discarded. We do this to prevent duplicate book entries which differ only in ISBN as shown in the following picture:

Screenshot of Views result

To remove the duplicate entries, follow this simple two step process:

  1. Find the correct Views hook to implement.
  2. Add the logic to remove unwanted results.

After reviewing the list of Views hooks, hook_views_pre_render is the one we are going to use to filter results before they are rendered. Now, let’s create a custom module to add the required logic. I have named my module views_alter_results so the hook implementation would look like this:

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  // Custom code.
}

The ampersand in the function parameter indicates that the View object is passed by reference. Any change we make to the object will be kept. The View object has a results property. Using the devel module, we can use dsm($view->results) to have a quick look at the results.

Screenshot of array result.

Each element in the array is a node that will be displayed in the final output. If we expand one of them, we can see more information about the node. Let’s drill down into one of the results until we get to the ISBN.

Screenshot of expanded array result.

The output will vary depending on your configuration. In this example, we have created a Book content type and added an ISBN field. Before adding the logic to filter the unwanted results, we need to make sure that this logic will only be applied for the specific view and display we are targeting. By default, hook_views_pre_render will be executed for every view and display unless otherwise instructed. We can apply this restriction as follows:

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  if ($view->name == 'books'
    && $view->current_display == 'page_book_list') {
    // Custom code.
  }
}

Next, the logic to filter results.

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  if ($view->name == 'books'
    && $view->current_display == 'page_book_list') {
    $isbn10_books = array();
    $isbn13_books = array();
    $remove_books = array();

    foreach ($view->result as $index => $value) {
      $isbn = $value->field_field_isbn[0]['raw']['value'];
      if (strlen($isbn) === 10) {
        // [184951116]0.
        $isbn10_books[$index] = substr($isbn, 0, 9);
      }
      elseif (strlen($isbn) === 13) {
        // 978[184951116]2.
        $isbn13_books[$index] = substr($isbn, 3, 9);
      }
    }

    // Find books that have both ISBN10 and ISBN13 entries.
    $remove_books = array_intersect($isbn10_books, $isbn13_books);

    // Remove repeated books.
    foreach ($remove_books as $index => $value) {
      unset($view->result[$index]);
    }
  }
}

To filter the results we use unset on $view->result. After this process, the result property of the view object will look like this:

Screenshot of array after unset action.

And our view will display without duplicates book entries as seen here:

Screenshot of view after being overriden by code.

Before wrapping up, I’d like to share two modules that might help you achieve similar results: Views Merge Rows and Views Distinct. Every use case is different, if neither of these modules gets you where you want to be, you can leverage hook_views_pre_render to implement your custom requirements.

Update #1 Tue, 06/02/2015

As indicated by Leon and efpapado this approach only works for views that present all results in a single page. That was the original use case. The altering presented here only affects the current page and the pager will not work as expected.

The grocery store was open for a brief time, but it was never a cooperative. I know, because I joined as a member the day it first opened in 2017, on August 11. (I first e-mailed to ask about becoming a member ten months earlier, but that required meeting in person and this was the first time it was feasible.)

On 2018, June 12, after Wirth Cooperative Grocery had been closed for two months, I received my only formal communication as a member owner: an e-mail acknowledging that the store was closed, noting that the current grocery market is extremely competitive, and saying they had plans to re-open by the end of the month.

The e-mail did not ask for decisions. It did not ask for volunteers or help of any kind. While addressed to us as member owners, it did not afford us any opportunity to affect the future of the store. It did not provide any information on which we might try to act. Instead it told us to wait to be notified when an opening date was set. An opening date was never set.

Although I'm certain some staff and volunteers worked hard behind the scenes, from my perspective as a member owner the grocery store went down without a fight.

That's why it's so important for cooperatives to be true cooperatives. The seven cooperative principles aren't a "pick any three" sort of deal.

The first principle specifies that membership in a cooperative be open to all people willing to accept the responsibilities, but a person cannot accept responsibilities which aren't offered.

The second principle is democratic member control, but people cannot exercise direct control or hold representatives accountable without information and without a means to communicate with one another.

Likewise for the next three cooperative principles: the third, that members democratically control capital; the fourth, that external agreements preserve democratic control; and the fifth, that a cooperative educate, train, and inform members so they can contribute to it effectively. An organization with no mechanisms for discussion nor for democracy violates these principles, too.

Principles six and seven, cooperation among cooperatives and concern for community, are likely to be hollow without functioning internal democracy— and certainly cannot be realized if the business fails.

A cooperative can't exist only on good intentions.

When I hear this sentiment expressed by experienced cooperators—founders and developers of cooperatives—it usually means that there needs to be a solid business model for a cooperative, because a cooperative that isn't economically viable can't fulfill any of its goals.

A more fundamental meaning is that a business can't be a cooperative if it merely intends to be; it must act like a cooperative. Otherwise, it's just another business with some co-op lip service gloss— and given the greater success of cooperatives compared to other businesses it's less likely to be around as a business at all, if it does not live up to its cooperative principles.

I'm not trying to use technicalities to dodge counting the failed Wirth grocery store as a failure for "team cooperative". On the contrary, this is a wakeup call for everybody who supports cooperatives, one that must rouse us, because fully-functioning cooperatives are bigger than the cooperative movement. Cooperatives can prefigure true democracy. We need to show that economic democracy works in our voluntary organizations; we need to give people in a world which is already ruled unjustly, and threatening to spiral out of control, a promise and a practice for how whole communities can take collective control.

In my experience as a member owner, Wirth Cooperative Grocery was not a co-op beyond its name and some vague intentions. Now I know that my experience matched everyone else's, thanks to Cirien Saadeh's reporting, both last year and in the most recent issue of North News (an invaluable local non-profit enterprise).

What worries me, then, is that no one quoted in these articles called out this failure to meet the basic requirements of being a cooperative. Minneapolis and Minnesota have perhaps the highest rate of cooperative membership in the United States, with about half of all residents estimated to belong to at least one cooperative of one kind or another. If we, here, don't have the awareness and interest to note when a cooperative doesn't act like a cooperative, who will?

More important than calling out failures is providing pathways to success. There are many programs and organizations supporting cooperatives in Minnesota and beyond, but none put ensuring member control first.

The bias of my profession and my passion is to lead with an online tool: ensure member owners can communicate with one another. Although a technological fix can't solve political problems, people who are affected need a way to talk among themselves to come up with a solution.

Efforts like a local cooperative grocery are small enough that a Loomio group or any mailing list would mostly work for member owner discussion. A better software application would work for collaborative groups of any size: members would filter messages for quality without ceding control to any outside group. This self-moderation for groups of equals, including cooperatives, is a goal of Visions Unite.

Are you in a position to provide resources to cooperatives and other groups seeking to grow and be democratic? Are you starting or do you want to start a cooperative or group? Do you have other ideas on how to help new and established cooperatives live by the foundational cooperative principles? I would like to hear from you!

In a previous article, we presented a list of properties per content entity in Drupal core and some contributed modules. This time we will provide a similar list for Drupal Commerce. When migrating into content entities, these define several properties that can be included in the process section to populate their values. For example, when importing Drupal Commerce product variations you can specify the SKU, price, list price, etc. In the case of promotions, you can set the start and end dates. Finding out which properties are available for an entity might require some Drupal development knowledge. To make the process easier, in today’s article we are presenting a reference of properties available in content entities provided by Drupal Commerce and some related contributed modules.

 

 

 

 

List of Drupal Commerce content entities.

For each entity we will present: the module that provides it, the class that defines it, and the available properties. For each property we will list its name, field type, a description, and a note if the field allows unlimited values (i.e. it has an unlimited cardinality). The list of properties available for a content entity depend on many factors. For example, if the entity is revisionable (e.g. revision_default), translatable (e.g. langcode), or both (e.g. revision_translation_affected). The modules that are enabled on the site can also affect the available properties. For instance, if the “Workspaces” module is installed, it will add a workspace property to many content entities. This reference assumes that Drupal was installed using the standard installation profile and only Drupal Commerce related modules that provide content entities are enabled.

It is worth noting that entity properties are divided in two categories: base field definitions and field storage configurations. Base field configurations will always be available for the entity. On the other hand, the presence of field storage configurations will depend on various factors. For one, they can only be added to fieldable entities. Attaching the fields to the entity can be done manually by the user, by a module, or by an installation profile. Again, this reference assumes that Drupal was installed using the standard installation profile with Drupal Commerce related modules enabled. By default, the commerce_product entity adds a bodyfield. For entities that can have multiple bundles, not all properties provided by the field storage configurations will be available in all bundles. For example, with the standard installation profile all content types will have a body field associated with it, but only the article content type has the field_image, and field_tags fields. If subfields are available for the field type, you can migrate into them.

If you are migrating into Drupal Commerce, make sure to check the Commerce Migrate module. It offers migrate destination field handlers for commerce fields and a plugin for commerce product types. It also provides a migration path from Commerce 1 (Drupal 7), Ubercart, and other e-commerce platforms. It is even possible to import data from other platforms like WooCommerce, Magento, and Shopify via CSV exports.

Store entity

Module: Commerce Store (part of commerce module)
Class: Drupal\commerce_store\Entity\Store

List of base field definitions:

  1. store_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. langcode: (language) Language.
  4. type: (entity_reference to commerce_store_type) Type. The store type.
  5. uid: (entity_reference to user) Owner. The store owner.
  6. name: (string) Name. The store name.
  7. mail: (email) Email. Store email notifications are sent from this address.
  8. default_currency: (entity_reference to commerce_currency) Default currency. The default currency of the store.
  9. timezone: (list_string) Timezone. Used when determining promotion and tax availability.
  10. address: (address) Address. The store address.
  11. billing_countries: (list_string) Supported billing countries. Allows unlimited values.
  12. path: (path) URL alias. The store URL alias.
  13. is_default: (boolean) Default. Whether this is the default store.
  14. default_langcode: (boolean) Default translation. A flag indicating whether this is the default translation.
  15. shipping_countries: (list_string) Supported shipping countries. Allows unlimited values.
  16. prices_include_tax: (boolean) Prices are entered with taxes included.
  17. tax_registrations: (list_string) Tax registrations. The countries where the store is additionally registered to collect taxes. Allows unlimited values.

Product entity

Module: Commerce Product (part of commerce module)
Class: Drupal\commerce_product\Entity\Product

List of base field definitions:

  1. product_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. langcode: (language) Language.
  4. type: (entity_reference to commerce_product_type) Product type.
  5. status: (boolean) Published.
  6. stores: (entity_reference to commerce_store) Stores. The product stores. Allows unlimited values.
  7. uid: (entity_reference to user) Author. The product author.
  8. title: (string) Title. The product title.
  9. variations: (entity_reference to commerce_product_variation) Variations. The product variations. Allows unlimited values.
  10. created: (created) Created. The time when the product was created.
  11. changed: (changed) Changed. The time when the product was last edited.
  12. default_langcode: (boolean) Default translation. A flag indicating whether this is the default translation.

List of field storage configurations:

  1. body: text_with_summary field.

Product variation entity

Module: Commerce Product (part of commerce module)
Class: Drupal\commerce_product\Entity\ProductVariation

List of base field definitions:

  1. variation_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. langcode: (language) Language.
  4. type: (entity_reference to commerce_product_variation_type) Product variation type.
  5. status: (boolean) Published.
  6. uid: (entity_reference to user) Author. The variation author.
  7. product_id: (entity_reference to commerce_product) Product. The parent product.
  8. sku: (string) SKU. The unique, machine-readable identifier for a variation.
  9. title: (string) Title. The variation title.
  10. list_price: (commerce_price) List price. The list price.
  11. price: (commerce_price) Price. The price
  12. created: (created) Created. The time when the variation was created.
  13. changed: (changed) Changed. The time when the variation was last edited.
  14. default_langcode: (boolean) Default translation. A flag indicating whether this is the default translation.

Product attribute value entity

Module: Commerce Product (part of commerce module)
Class: Drupal\commerce_product\Entity\ProductAttributeValue

List of base field definitions:

  1. attribute_value_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. langcode: (language) Language.
  4. attribute: (entity_reference to commerce_product_attribute) Attribute.
  5. name: (string) Name. The attribute value name.
  6. weight: (integer) Weight. The weight of this attribute value in relation to others.
  7. created: (created) Created. The time when the attribute value was created.
  8. changed: (changed) Changed. The time when the attribute value was last edited.
  9. default_langcode: (boolean) Default translation. A flag indicating whether this is the default translation.

Order entity

Module: Commerce Order (part of commerce module)
Class: Drupal\commerce_order\Entity\Order

List of base field definitions:

  1. order_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. type: (entity_reference to commerce_order_type) Order type.
  4. order_number: (string) Order number. The order number displayed to the customer.
  5. store_id: (entity_reference to commerce_store) Store. The store to which the order belongs.
  6. uid: (entity_reference to user) Customer. The customer.
  7. mail: (email) Contact email. The email address associated with the order.
  8. ip_address: (string) IP address. The IP address of the order.
  9. billing_profile: (entity_reference_revisions) Billing information. Billing profile
  10. order_items: (entity_reference to commerce_order_item) Order items. The order items. Allows unlimited values.
  11. adjustments: (commerce_adjustment) Adjustments. Allows unlimited values.
  12. total_price: (commerce_price) Total price. The total price of the order.
  13. total_paid: (commerce_price) Total paid. The total paid price of the order.
  14. state: (state) State. The order state.
  15. data: (map) Data. A serialized array of additional data.
  16. locked: (boolean) Locked.
  17. created: (created) Created. The time when the order was created.
  18. changed: (changed) Changed. The time when the order was last edited.
  19. placed: (timestamp) Placed. The time when the order was placed.
  20. completed: (timestamp) Completed. The time when the order was completed.
  21. cart: (boolean) Cart.
  22. checkout_flow: (entity_reference to commerce_checkout_flow) Checkout flow.
  23. checkout_step: (string) Checkout step.
  24. payment_gateway: (entity_reference to commerce_payment_gateway) Payment gateway. The payment gateway.
  25. payment_method: (entity_reference to commerce_payment_method) Payment method. The payment method.
  26. coupons: (entity_reference to commerce_promotion_coupon) Coupons. Coupons that have been applied to order. Allows unlimited values.

Order item entity

Module: Commerce Order (part of commerce module)
Class: Drupal\commerce_order\Entity\OrderItem

List of base field definitions:

  1. order_item_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. type: (entity_reference to commerce_order_item_type) Order item type.
  4. order_id: (entity_reference to commerce_order) Order. The parent order.
  5. purchased_entity: (entity_reference to node) Purchased entity. The purchased entity.
  6. title: (string) Title. The order item title.
  7. quantity: (decimal) Quantity. The number of purchased units.
  8. unit_price: (commerce_price) Unit price. The price of a single unit.
  9. overridden_unit_price: (boolean) Overridden unit price. Whether the unit price is overridden.
  10. total_price: (commerce_price) Total price. The total price of the order item.
  11. adjustments: (commerce_adjustment) Adjustments. Allows unlimited values.
  12. uses_legacy_adjustments: (boolean) Uses legacy adjustments.
  13. data: (map) Data. A serialized array of additional data.
  14. created: (created) Created. The time when the order item was created.
  15. changed: (changed) Changed. The time when the order item was last edited.

Payment method entity

Module: Commerce Payment (part of commerce module)
Class: Drupal\commerce_payment\Entity\PaymentMethod

List of base field definitions:

  1. method_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. type: (string) Payment method type.
  4. payment_gateway: (entity_reference to commerce_payment_gateway) Payment gateway. The payment gateway.
  5. payment_gateway_mode: (string) Payment gateway mode. The payment gateway mode.
  6. uid: (entity_reference to user) Owner. The payment method owner.
  7. remote_id: (string) Remote ID. The payment method remote ID.
  8. billing_profile: (entity_reference_revisions) Billing profile. Billing profile
  9. reusable: (boolean) Reusable. Whether the payment method is reusable.
  10. is_default: (boolean) Default. Whether this is the user's default payment method.
  11. expires: (timestamp) Expires. The time when the payment method expires. 0 for never.
  12. created: (created) Created. The time when the payment method was created.
  13. changed: (changed) Changed. The time when the payment method was last edited.

Payment entity

Module: Commerce Payment (part of commerce module)
Class: Drupal\commerce_payment\Entity\Payment

List of base field definitions:

  1. payment_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. type: (string) Payment type.
  4. payment_gateway: (entity_reference to commerce_payment_gateway) Payment gateway. The payment gateway.
  5. payment_gateway_mode: (string) Payment gateway mode. The payment gateway mode.
  6. payment_method: (entity_reference to commerce_payment_method) Payment method. The payment method.
  7. order_id: (entity_reference to commerce_order) Order. The parent order.
  8. remote_id: (string) Remote ID. The remote payment ID.
  9. remote_state: (string) Remote State. The remote payment state.
  10. amount: (commerce_price) Amount. The payment amount.
  11. refunded_amount: (commerce_price) Refunded amount. The refunded payment amount.
  12. state: (state) State. The payment state.
  13. authorized: (timestamp) Authorized. The time when the payment was authorized.
  14. expires: (timestamp) Expires. The time when the payment expires. 0 for never.
  15. completed: (timestamp) Completed. The time when the payment was completed.
  16. test: (boolean) Test. Whether this is a test payment.
  17. captured: (timestamp) Captured. The time when the payment was captured.

Promotion entity

Module: Commerce Promotion (part of commerce module)
Class: Drupal\commerce_promotion\Entity\Promotion

List of base field definitions:

  1. promotion_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. langcode: (language) Language.
  4. name: (string) Name. The promotion name.
  5. display_name: (string) Display name. If provided, shown on the order instead of "Discount".
  6. description: (string_long) Description. Additional information about the promotion to show to the customer
  7. order_types: (entity_reference to commerce_order_type) Order types. The order types for which the promotion is valid. Allows unlimited values.
  8. stores: (entity_reference to commerce_store) Stores. The stores for which the promotion is valid. Allows unlimited values.
  9. offer: (commerce_plugin_item:commerce_promotion_offer) Offer type.
  10. conditions: (commerce_plugin_item:commerce_condition) Conditions. Allows unlimited values.
  11. condition_operator: (list_string) Condition operator. The condition operator.
  12. coupons: (entity_reference to commerce_promotion_coupon) Coupons. Coupons which allow promotion to be redeemed. Allows unlimited values.
  13. usage_limit: (integer) Usage limit. The maximum number of times the promotion can be used. 0 for unlimited.
  14. start_date: (datetime) Start date. The date the promotion becomes valid.
  15. end_date: (datetime) End date. The date after which the promotion is invalid.
  16. compatibility: (list_string) Compatibility with other promotions.
  17. status: (boolean) Status. Whether the promotion is enabled.
  18. weight: (integer) Weight. The weight of this promotion in relation to others.
  19. default_langcode: (boolean) Default translation. A flag indicating whether this is the default translation.

Coupon entity

Module: Commerce Promotion (part of commerce module)
Class: Drupal\commerce_promotion\Entity\Coupon

List of base field definitions:

  1. id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. promotion_id: (entity_reference to commerce_promotion) Promotion. The parent promotion.
  4. code: (string) Coupon code. The unique, machine-readable identifier for a coupon.
  5. usage_limit: (integer) Usage limit. The maximum number of times the coupon can be used. 0 for unlimited.
  6. status: (boolean) Status. Whether the coupon is enabled.

Log entity

Module: Commerce Log (part of commerce module)
Class: Drupal\commerce_log\Entity\Log

List of base field definitions:

  1. log_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. uid: (entity_reference to user) User. The user for the log.
  4. template_id: (string) Log template ID. The log template plugin ID
  5. category_id: (string) Log category ID. The log category plugin ID
  6. source_entity_id: (integer) Source entity ID. The source entity ID
  7. source_entity_type: (string) Source entity type. The source entity type
  8. params: (map) Params. A serialized array of parameters for the log template.
  9. created: (created) Created. The time when the log was created.

Price list entity

Module: Commerce Pricelist
Class: Drupal\commerce_pricelist\Entity\PriceList

List of base field definitions:

  1. id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. type: (string) Price list bundle.
  4. uid: (entity_reference to user) Owner. The user that owns this price list.
  5. name: (string) Name. The name of the price list.
  6. stores: (entity_reference to commerce_store) Stores. The stores for which the price list is valid. Allows unlimited values.
  7. customer: (entity_reference to user) Customer. The customer for which the price list is valid.
  8. customer_roles: (entity_reference to user_role) Customer roles. The customer roles for which the price list is valid. Allows unlimited values.
  9. start_date: (datetime) Start date. The date the price list becomes valid.
  10. end_date: (datetime) End date. The date after which the price list is invalid.
  11. weight: (integer) Weight. The weight of this price list in relation to other price lists.
  12. status: (boolean) Status. Whether the price list is enabled.
  13. changed: (changed) Changed. The time when the price list was last edited.

Price list item entity

Module: Commerce Pricelist
Class: Drupal\commerce_pricelist\Entity\PriceListItem

List of base field definitions:

  1. id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. type: (string) Price list item bundle.
  4. uid: (entity_reference to user) Owner. The user that owns this price list item.
  5. price_list_id: (entity_reference to commerce_pricelist) Price list. The parent price list.
  6. purchasable_entity: (entity_reference to commerce_product_variation) Purchasable entity. The purchasable entity.
  7. quantity: (decimal) Quantity. The quantity tier.
  8. list_price: (commerce_price) List price. The list price.
  9. price: (commerce_price) Price. The price.
  10. status: (boolean) Status. Whether the price list item is enabled.
  11. changed: (changed) Changed. The time when the price list item was last edited.

Shipment entity

Module: Shipping
Class: Drupal\commerce_shipping\Entity\Shipment

List of base field definitions:

  1. shipment_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. type: (entity_reference to commerce_shipment_type) Shipment type.
  4. order_id: (entity_reference to commerce_order) Order. The parent order.
  5. package_type: (string) Package type. The package type.
  6. shipping_method: (entity_reference to commerce_shipping_method) Shipping method. The shipping method
  7. shipping_service: (string) Shipping service. The shipping service.
  8. shipping_profile: (entity_reference_revisions) Shipping information.
  9. title: (string) Title. The shipment title.
  10. items: (commerce_shipment_item) Items. Allows unlimited values.
  11. weight: (physical_measurement) Weight.
  12. original_amount: (commerce_price) Original amount. The original amount.
  13. amount: (commerce_price) Amount. The amount.
  14. adjustments: (commerce_adjustment) Adjustments. Allows unlimited values.
  15. tracking_code: (string) Tracking code. The shipment tracking code.
  16. state: (state) State. The shipment state.
  17. data: (map) Data. A serialized array of additional data.
  18. created: (created) Created. The time when the shipment was created.
  19. changed: (changed) Changed. The time when the shipment was last updated.
  20. shipped: (timestamp) Shipped. The time when the shipment was shipped.

Shipping method entity

Module: Shipping
Class: Drupal\commerce_shipping\Entity\ShippingMethod

List of base field definitions:

  1. shipping_method_id: (integer) ID.
  2. uuid: (uuid) UUID.
  3. langcode: (language) Language.
  4. stores: (entity_reference to commerce_store) Stores. The stores for which the shipping method is valid. Allows unlimited values.
  5. plugin: (commerce_plugin_item:commerce_shipping_method) Plugin.
  6. name: (string) Name. The shipping method name.
  7. conditions: (commerce_plugin_item:commerce_condition) Conditions. Allows unlimited values.
  8. condition_operator: (list_string) Condition operator. The condition operator.
  9. weight: (integer) Weight. The weight of this shipping method in relation to others.
  10. status: (boolean) Enabled. Whether the shipping method is enabled.
  11. default_langcode: (boolean) Default translation. A flag indicating whether this is the default translation.

Available properties for other content entities

This reference includes Drupal Commerce content entities and some provided by related contributed modules. The previous article included a reference for Drupal core content entities. That being said, it would be impractical to cover all contributed modules. To get a list of yourself for other content entities, load the entity_type.manager service and call its getFieldStorageDefinitions() method passing the machine name of the entity as a parameter. Although this reference only covers content entities, the same process can be used for configuration entities.

What did you learn in today’s article? Did you know that there were so many entity properties provided by Drupal Commerce? Were you aware that the list of available properties depend on factors like if the entity is fieldable, translatable, and revisionable? Did you know how to find properties for content entities from contributed modules? Please share your answers in the comments. Also, we would be grateful if you shared this article with your friends and colleagues.

 

 

 

 

 

 

 

 

Functionality designed to your life is the Agaric Design signature. Utilizing open source, free software from around the world, Agaric Design websites are impeccably crafted with a modern, sophisticated and understated spirit.

Our guiding philosophy is to bring open source and usability that works effortlessly in your day to day life.

I know we will become a favorite in your contacts list. And thank you! I consider that the ultimate compliment.

Dan Hakimzadeh

When all goes according to plan—which is surprisingly often—theming in Drupal 8 is a straightforward matter of editing templates and stylesheets. We found things did not go according to plan when styling page-level action links (such as "Add new Forum topic") and content-level action links (or node links as Drupal 8 still calls them, such as "read more" or "add comment"). We are going to show how to add Bootstrap-specific styles, but the same approaches would be useful to add special stylings, button-like or otherwise, to selected action links and node links.

First, let's revisit the steps to follow when trying to add classes to Drupal-produced markup:

  1. Enable Twig debugging.
  2. Using your browser's inspector, identify the template providing the code you need to modify.
  3. Copy the template from where Twig debug output says it lives in Drupal core (or contrib) to your theme. If needed, use template name suggestions with a --modifier to selectively override the template in specific cases.
  4. Add BEM-compliant classes to your template. This is usually accomplished by tacking the addClass("here are--my-classes") method onto an attributes variable.
  5. Use the classes you added in your CSS.

Now let's try applying that to styling action links and node links (but only the actual link parts, not the surrounding text) as buttons.

Page-level action links

Here is an action link provided by Drupal core in the Forum module. It's classes don't align with Bootstrap's, so it displays without style.

A "Forums" headline with an unstyled "Add new Forum topic" action link below it.

The process

If we open the template that is providing the HTML for each link, menu-local-action.html.twig, it is only one line of code (and 12 lines of comments). It couldn't be simpler!

{#
/**
 * @file
 * Default theme implementation for a single local action link.
 *
 * Available variables:
 * - attributes: HTML attributes for the wrapper element.
 * - link: A rendered link element.
 *
 * @see template_preprocess_menu_local_action()
 *
 * @ingroup themeable
 */
#}
<li{{ attributes }}>{{ link }}</li>

Except... the attributes variable we have available is on the list item (li tag), not the link itself. Using this template we can't add classes to the already-rendered link element. Putting the button class on the list item would result in a common UX problem: button-looking elements with parts that are not clickable.

Even though this template cannot be used directly, it points us in the right direction. On line 10, a comments suggest us to see template_preprocess_menu_local_action(). So we shall.

A symbol finder in an IDE or grep will quickly take us to line 65 of core/includes/menu.inc:

/**
 * Prepares variables for single local action link templates.
 *
 * Default template: menu-local-action.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: A render element containing:
 *     - #link: A menu link array with 'title', 'url', and (optionally)
 *       'localized_options' keys.
 */
function template_preprocess_menu_local_action(&$variables) {
  $link = $variables['element']['#link'];
  $link += array(
    'localized_options' => array(),
  );
  $link['localized_options']['attributes']['class'][] = 'button';
  $link['localized_options']['attributes']['class'][] = 'button-action';
  $link['localized_options']['set_active_class'] = TRUE;

  $variables['link'] = array(
    '#type' => 'link',
    '#title' => $link['title'],
    '#options' => $link['localized_options'],
    '#url' => $link['url'],
  );
}

Here we can see exactly how Drupal is adding classes ('button' and 'button-action') to the buttons. Let's add our own preprocess function assuming our theme is name exampletheme:

  1. Add a function to our .theme file. In this example, the file would be exampletheme.theme.
  2. Name the function exampletheme_preprocess_menu_local_action(). That is, replace the word 'template' with the name of our theme name.
  3. Modify the $variables array to add our classes.

We could even remove the existing classes from the link, but we'll leave them for now. Note that the link that gets processed is $variables['link'] rather than $variables['element']['#link'].

The solution

/**
 * Extends template_preprocess_menu_local_action().
 *
 * Add Bootstrap button classes to a single local action link.
 *
 * Default template: menu-local-action.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: A render element containing:
 *     - #link: A menu link array with 'title', 'url', and (optionally)
 *       'localized_options' keys.
 */
function exampletheme_preprocess_menu_local_action(&$variables) {
  $variables['link']['#options']['attributes']['class'][] = 'btn';
  $variables['link']['#options']['attributes']['class'][] = 'btn-success';
}

A forums listing page with a large green button labeled "Add new Forum topic" at the top.

Content-level action links

Next let's style node links as buttons. It's well-nigh impossible to get the btn and btn-success classes on the login and register links within the sentence "Log in or register to post comments". Therefore, we will use Bootstrap's handy mixins. The following is a SCSS code snippet which is turned into CSS by a SASS preprocessor.

.links--node a {
  @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border);
  @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base);
}

Finally, we just need to add the links--node class. Assuming our theme is called exampletheme:

/**
 * Implements hook_preprocess_links() for node entities.
 */
function exampletheme_preprocess_links__node(&$variables) {
  $variables['attributes']['class'][] = 'list-unstyled';
  $variables['attributes']['class'][] = 'links--node';
}

A comments section noting there are no comments yet, and providing two links, styled as buttons, in the sentence 'Log in or register to post comments'

Bonus: Link field links as buttons

For styling the output of link fields as buttons, the aptly-named Button link formatter module can help you out without the need for custom code nor templating.

Are you styling action links, node links, and other links?

Have you faced similar needs for changing the look of links, to be buttons or otherwise? How have you met them? Let us know in the comments!

 

Abico aptent erat gilvus immitto lenis nisl os pala virtus. Augue caecus neo nostrud pagus secundum valetudo vero. Dolore jumentum melior quae ullamcorper. Abbas exputo iusto modo quae ullamcorper uxor vulputate. Abbas aliquam aliquip inhibeo mos vel. Abbas aliquam brevitas commodo dolor macto populus premo sino. Abico aliquip causa consectetuer diam dolus imputo jugis obruo olim.

Accumsan caecus eros esca eu huic letalis metuo vereor zelus. Immitto macto os pneum praemitto sudo vereor. Aliquam loquor luptatum nulla pertineo praemitto tum ut virtus.

Eros te usitas. Diam distineo erat facilisi humo lucidus nulla occuro quibus vel. Abluo eu jumentum lucidus minim neque quidem tamen ymo. Commoveo defui eros pecus quadrum sed si tincidunt uxor.

Abbas facilisis nibh nisl similis tation. Appellatio ex ibidem immitto pala valde. Decet esca nutus oppeto pecus si tum valde venio. Dolore facilisis inhibeo interdico molior te. Dolor eros facilisi facilisis genitus haero letalis obruo torqueo. Abluo at conventio diam duis eros iusto molior sudo ymo.

Acsi comis mauris saluto. Augue brevitas caecus neo nibh tamen validus. Blandit eligo eu hos quia scisco voco volutpat ymo. Distineo erat gilvus lenis quae vero. Capto consequat iaceo in jus quae ullamcorper. Humo magna nutus os quadrum singularis usitas.

Caecus distineo neque si ymo. Exerci hos pagus similis velit. Abbas aptent brevitas capto hendrerit in pertineo. Acsi hendrerit occuro odio roto saluto sed tum vicis zelus. Duis exerci jumentum meus nisl oppeto premo proprius sudo suscipere. Capto mauris neque secundum. Cogo cui et pneum torqueo. Euismod iusto neo nostrud. Hendrerit usitas volutpat.

Aptent caecus ibidem loquor nulla secundum tamen. Facilisis iriure iustum paratus tincidunt. Quidne turpis ullamcorper. Accumsan aliquip at facilisi premo sit tego torqueo virtus. Hos incassum iustum lucidus nobis plaga. Dolus esca eum jus validus ymo. Ad bene consectetuer erat obruo utinam virtus. Abico comis cui exerci incassum scisco. Diam pertineo sed sudo. At facilisi mauris praemitto quis vulputate.

Adipiscing brevitas dignissim hendrerit os qui suscipere vereor. Diam huic natu nisl veniam. Abdo eros iaceo iustum lobortis melior oppeto probo tum vulputate. Feugiat genitus nimis. Diam eligo elit esca fere laoreet paratus tamen. Conventio defui diam elit obruo oppeto pagus premo voco volutpat.

Dolore feugiat jumentum obruo proprius quadrum validus velit verto. Antehabeo in lenis. Erat praesent quidem vulpes wisi. Consectetuer jumentum mos vindico. Abico causa fere humo nisl olim sudo suscipit ulciscor venio. Abigo ea eu nutus suscipit. Consequat genitus humo metuo minim nibh roto rusticus tincidunt utrum. Esse lenis pecus tum turpis usitas. Dolor et ibidem lobortis melior olim tum utinam vulpes.

Accumsan eligo esca gemino importunus in ludus melior paulatim tego. Acsi camur feugiat iriure quae sed ymo. Eum meus nunc quidne virtus. Caecus jumentum neque suscipit wisi. Cui duis eu haero letalis ludus plaga ut valde. Acsi eligo immitto quibus si tum. Antehabeo tincidunt wisi. Blandit facilisis importunus laoreet.

Accumsan hos si. Enim letalis minim suscipit valde valetudo vicis. Comis occuro torqueo ullamcorper. At blandit dolus haero hendrerit ille jus verto. Abluo at consequat immitto in olim sed volutpat. At nibh velit. Autem esse facilisis secundum sudo. At augue incassum quidne voco vulpes. Melior paratus scisco. Dignissim haero modo molior sed vereor vulpes.

 

Sign up to be notified when Agaric gives an online or in-person migration training:

Learning objectives

  • Step through the Drupal installation process.
  • Create nodes and understand their properties.
  • Create content types and add fields.
  • Configure fields of different types.
  • Create taxonomy vocabularies and terms.
  • Create views to display listings of content.
  • Create, place, and control the visibility of blocks.
  • Override the presentation of nodes with Layout Builder.
  • Understand the user and permission system.
  • Understand the taxonomy system.
  • Understand the menu system.
  • Install and configure contributed modules and themes.
  • Understand how Drupal can be extended to meet specific project needs.

Prerequisites

No previous experience with Drupal is needed.

Setup instructions

A working Drupal 11 installation using DDEV will be provided.

This training will be provided over Zoom. You can ask questions via text chat or audio. Sharing your screen is optional, but you might want to do it to get assistance on a specific issue. Sharing your camera is optional.

What to expect

Women looking at map

Prior to the training

Attendees will receive detailed instructions on how to setup their development environment. In addition, they will be able to join a support video call days before the training event to make the the local development environment is ready. This prevents losing time fixing problems with environment set up during the training.

On the days of the training

  • The training totals 7 hours of instruction, which we usually split into 2 sessions
  • A team of developers available to answer questions and help with training-related issues

After the training

  • Attendees will receive a copy of the training recording.
  • Attendees will receive a certificate of completion.

Using a collaborative, co-design process grounded in design justice principles, Agaric will move your website to have mobile-first design, powerful content management capabilities, and built-in accessibility. The site will continually be improved with new features, strengthened security, and more joyous experience of use.

Aproach

Agaric adheres to high standards of collaboration, communication, active client participation, and transparency.

Using design and development processes that center the people who will be affected by the website, a core team from Agaric will work closely with you to define overall goals, key outcomes, and user stories.

Through collaborative design, which will proceed iteratively throughout the project, we will identify and build a minimum viable product, and subject that to a round of feedback and further research. In this context, as Zora Neale Hurston put it, "Research is formalized curiosity. It is poking and prying with a purpose." Critically, this is research with people using the website. We then take what we learned and use that to identify and build the next minimum viable improvement.

This learning is not just for us building the site but for everyone who has a stake in success for the goals for which the site is being built. The learning covers both the website's usability and its effectiveness in reaching your goals, including effectiveness in communicating to colleagues and the general public your values and goals. Ideally, Agaric's research with you and the people who are or will be visiting the site is continuous and collaborative, constantly informing continued development.

In the steps of clarifying need and customizing the solution (leveraging state of the art open source libre software), we prefer to write and work off of user stories that clearly define the website's purpose. This helps ensure that product owners, designers, and developers are on the same page. User stories describe software functionality from the perspective of the person using the software, including motivation or benefit, such as "A visitor can ".

We use an agile methodology to prioritize with you to put critical functionality first, getting a functional website in your hands as soon as possible for review. We continue this iterative and collaborative development cycle, typically in two week sprints, always building the highest impact functionality first.

Comparative analysis

No one is doing exactly what you will do, but identifying quality peers and reviewing their respective content helps you get a wider perspective both on what potential visitors may have seen elsewhere and what seems to be working for others— we can start thinking together about where to emulate and where to differentiate, informing all aspects of building the site to achieve your goals needs.

Content strategy

Building on the review of competitors, Agaric will briefly interview people who should be interacting with your site (from content creators to potential visitors) and develop personas and user stories.

Content style guide

Along with bringing consistency to cooperative output (and saving time sweating the details every time they come up), a good content (copywriting) style guide incorporates suggestions for clear and effective writing and helps your unique aspects shine through. It can help tell your story in a consistent way and help let your individual personalities show through while maintaining collective coherence.

Agaric's content style guide has not received the dedicated attention we recommend for our clients, but can still be a useful starting point for you.

Services

From our discussion and review of your goals and audiences we recommend the following services, to be provided by Agaric in collaboration with the Drutopia cooperative web platform. Our agile method allows for flexibility throughout the project.

The Drutopia initiative builds powerful website software for grassroots groups and offers a Libre Software as a Service platform to make getting a website even easier for these groups. Drutopia members benefit from the collective development and maintenance of the software and platform; currently under the aegis of a not-for-profit, Drutopia plans to grow into its own cooperative.

May First Movement Technology is a non-profit cooperative which builds movements by advancing the strategic use and collective control of technology for local struggles, global transformation, and emancipation without borders. Together, through the cooperative, members buy information technology equipment to run websites, email, email lists, and just about everything else we do on the Internet. Drutopia is hosted on May First infrastructure, and Drutopia members become May First members.

Building on and with these cooperative initiatives, Agaric can focus our energy towards custom design or development that will benefit you.

Included Features

Cutting-Edge Website Capabilities

As a member organization of Drutopia, the your website will automatically gain access to the following features:

  • Configurable homepage - that include but is not limited to any of the following: prominent elevator pitch, recent news feed, featured resources, upcoming events, partner organizations list, and a list of current campaigns.
  • Actions
  • Campaigns
  • Events
  • Fundraising
  • Groups
  • News and/or Blog
  • Profile pages
  • Social media integration
  • SSL (all pages on the site reachable at secure, privacy-protecting https:// URLs)

Highly Secure Internal Tools

In addition to these benefits, we recommend [membership in May First Movement Technology] and will discount your Drutopia membership by the amount of a standard organization membership in May First ($50/year). As a result of this membership, you will gain access to additional services beyond the website, including the following:

  • E-mail boxes and forwards
  • Webmail
  • NextCloud Files and Gallery (similar to Dropbox or Google Drive)
  • OnlyOffice (integrated with NextCloud for online editing of word documents and spreadsheets)
  • NextCloud Calendar and Tasks
  • NextCloud Contacts
  • NextCloud Bookmarks and Notes
Content Strategy and Information Architecture Approach

We will work closely with your team to define an effective content strategy for your website and a clean, scalable information architecture. These will result in the following deliverables:

  • Website goals
  • User personas
  • Content style guide
  • Sitemap
  • Technical architecture
Design Approach

Building off of the content strategy and information architecture work, we will work with your team to provide a compelling a theme that speaks to your audience and reinforces your brand.

We begin with our clean, responsive theme, with the following features baked in:

  • Clean - simple design that puts your content first
  • Secure - covered by the Drupal security team
  • Accessible - works with assistive devices and keyboard-only navigation
  • Responsive - looks beautiful on any device
  • Extensible - well organized templates and Sass (style sheets)

We then identify and implement the customizations that will set your website apart and best tell your story, reflecting the unique voice and tone of your organization.

Development Approach

By leveraging Drutopia, we are able to quickly prototype with your team. After defining the information architecture we turn on all relevant features, and stub out the pages in the sitemap. We put on a collaborative training on how to work with the site and then allow your team to add content, test out the features and make note of any adjustments that would be helpful to the project.

We will then work closely together to prioritize remaining enhancements to be made to the website. Enhancements that do not make it into the initial build are added to our backlog for the Drutopia project as a whole.

Content Migration

Agaric staff will work with the you to move content from an existing website to your .

This plan will consist of manual migration work to move content from existing pages into the new website. Additionally, optional programmatic migration processes are available for moving user data and other content into Drupal. A redirect strategy will also be put in place for pages that are being culled from the old site.

Accessibility

It is critical that your site be accessible to as many people as possible, including those using screen readers. To that end, all of our work is built to support compliance with W3C Web Content Accessibility Guidelines (WCAG) 2.0. The WCAG Guidelines, however, are not comprehensive and so we go beyond those guidelines to ensure high accessibility. Lastly, much of what makes a site accessible happens on the content entry and management side. We will provide resources for your content team so that after the site launches, you can rest assured that what you are creating is reaching as many people as possible.

SEO

Our developers will also follow SEO best-practice development and utilize Drupal's range of SEO-related features that allow administrators to edit page titles, implement human-readable and editable URLs, enter meta tags, and more. For further SEO-related services, we can also recommend SEO consultants.

Drutopia Membership and Continuous Improvements & Support

Launching with a site on Drutopia means more than getting all of the features and functionality Drutopia has to offer. It also means joining a platform cooperative of like-minded people. The cooperative consists of a one member one vote model and an accountable leadership team. Joining the Drutopia platform includes:

  • Secure hosting with timely security updates
  • The ability to request new features and endorse existing ideas
  • Quarterly updates on the project
  • Ability to coordinate with fellow members to crowdfund special development sprints

When joining Drutopia your site launch is not the end, but rather the beginning. Drutopia is in continual communication with all its members about what enhancements team members should work on next. Drutopia's roadmap is public to all so you know where the project is heading.

Agaric, as both your partner in maintaining your website and as a Drutopia partner, is well positioned to help see your priorities become realized.

Deliverables and Budget

Content Strategy documents

Information Architecture

Design

Development

Migrating content into a Drutopia typically takes an additional 10 to 50 hours ($1,500 to $15,000), varying

We are experts in Drupal migrations. We can move content from your old site to your Drutopia site so that you can keep working with all of your old content, all while gaining access to the flexibility and functionality of Drupal.

For a potentially much lower cost option, in four hours ($600) we can make a static archive of your old site, remove interactive features, add a link to the new site, place the old site at a subdomain, and configure your site to allow visitors looking for pages that exist on the old site but not on the new site to pass through and reach the old site.

We do not charge extra for hosting the static site.

Drutopia Membership, Hosting & Services $50/month or $500/year.

There will be no license fees for any software.

Agaric, founded in 2006, is a worker-owned web development cooperative headquartered in Boston (USA), with worker-owners also in Minneapolis and in Managua, Nicaragua. We help organizations meet their goals and strengthen the free software movement by providing consulting in online technology strategy, by building and customizing high quality software, by training people and by speaking at events. We use design justice principles to help your online presence meet your goals and make real world impact. We build with proven software that gives you power and control over your website and online presence. We use and contribute to libre software whenever possible, creative commons license our documentation, and work under an open organization model.

We look forward to creating a better website for building a better world with you!

CKEditor is well-known software with a big community behind it and it already has a ton of useful plugins ready to be used. It is the WYSIWYG text editor which ships with Drupal 8 core.

Unfortunately, the many plugins provided by the CKEditor community can't be used directly in the CKEditor that comes with Drupal 8. It is necessary to let Drupal know that we are going to add a new button to the CKEditor.

Why Drupal needs to know about our plugins

Drupal allows us to create different text formats, where depending on the role of the user (and so what text formats they have available) they can use different HTML tags in the content. Also, we can decide if the text format will use the CKEditor at all and, if it does, which buttons will be available for that text format.

That is why Drupal needs to know about any new button, so it can build the correct configuration per text format.

Adding a new button to CKEditor

We are going to add the Media Embed plugin, which adds a button to our editor that opens a dialog where you can paste an embed code from YouTube, Vimeo, and other providers of online video hosting.

First of all, let's create a new module which will contain the code of this new button, so inside the /modules/contrib/ folder let's create a folder called wysiwyg_mediaembed. (If you're not intending to share your module, you should put it in /modules/custom/— but please share your modules, especially ones making CKEditor plugins available to Drupal!)

cd modules/contrib/
mkdir wysiwyg_mediaembed

And inside let's create the info file: wysiwyg_mediaembed.info.yml

name: CKEditor Media Embed Button (wysiwyg_mediaembed)
type: module
description: "Adds the Media Embed Button plugin to CKEditor."
package: CKEditor
core: '8.x'
dependencies:
  - ckeditor

Adding this file will Drupal allows us to install the module, if you want to read more about how to create a custom module, you can read about it here.

Once we have our info file we just need to create a Drupal plugin which will give info to the CKEditor about this new plugin, we do that creating the following class:

touch src/Plugin/CkEditorPlugin/MediaEmbedButton.php

With this content:

namespace Drupal\wysiwyg_mediaembed\Plugin\CKEditorPlugin;

use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\editor\Entity\Editor;

/**
 * Defines the "wysiwyg_mediaembed" plugin.
 *
 * @CKEditorPlugin(
 *   id = "mediaembed",
 *   label = @Translation("CKEditor Media Embed Button")
 * )
 */
class MediaEmbedButton extends CKEditorPluginBase {

  /**
   * Get path to library folder.
   * The path where the library is, usually all the libraries are
   * inside the '/libraries/' folder in the Drupal root.
   */
  public function getLibraryPath() {
    $path = '/libraries/mediaembed';
    return $path;
  }

  /**
   * {@inheritdoc}
   * Which other plugins require our plugin, in our case none.
   */
  public function getDependencies(Editor $editor) {
    return [];
  }

  /**
   * {@inheritdoc}
   * The path where CKEditor will look for our plugin.
   */
  public function getFile() {
    return $this->getLibraryPath() . '/plugin.js';
  }

  /**
   * {@inheritdoc}
   *
   *  We can provide extra configuration if our plugin requires
   *  it, in our case we no need it.
   */
  public function getConfig(Editor $editor) {
    return [];
  }

  /**
   * {@inheritdoc}
   * Where Drupal will look for the image of the button.
   */
  public function getButtons() {
    $path = $this->getLibraryPath();
    return [
      'MediaEmbed' => [
        'label' => $this->t('Media Embed'),
        'image' => $path . '/icons/mediaembed.png',
      ],
    ];
  }
}

The class's code is pretty straightforward: it is just a matter of letting Drupal know where the library is and where the button image is and that's it.

The rest is just download the library and put it in the correct place and activate the module. If all went ok we will see our new button in the Drupal Text Format Page (usually at: /admin/config/content/formats).

This module was ported because we needed it in a project, so if you want to know how this code looks all together, you can download the module from here.

Now that you know how to port a CKEditor plugin to Drupal 8 the next time you can save time using Drupal Console with the following command:

drupal generate:plugin:ckeditorbutton

What CKEditor plugin are you going to port?

We believe in the freedom to move our data to whichever platforms best meets our needs. For that reason, we specialize in helping people migrate onto liberating software such as Drupal— and helping you learn how to do the same.

As the COVID-19 crisis continues, we will be announcing online trainings soon!

Join our trainings announcement list to be the first to get news of training opportunities: