Drupal Planet

Subscribe to Drupal Planet feed
Drupal.org - aggregated feeds in category Planet Drupal
Updated: 3 hours 15 min ago

Web Omelette: Dynamic menu links in Drupal 8 with plugin derivatives

June 5, 2017 - 12:35pm

Drupal 8 has become much more flexible for doing pretty much everything. In this article I want to talk a bit about menu links and show you how powerful the new system is compared to Drupal 7.

In Drupal 7, menu links were a thing of their own with an API that you can use to create them programatically and put them in a menu. So if you wanted to deploy a menu link in code, you’d have to write an update hook and create the link programatically. All in a day’s…

We have much more control in Drupal 8. First, it has become significantly easier to do the same thing. Menu links are now plugins discovered from YAML files. So for example, to define a link in code, all you need is place the following inside a my_module.links.menu.yml file:

my_module.link_name: title: 'This is my link' description: 'See some stuff on this page.' route_name: my_module.route_it_points_to parent: my_module.optional_parent_link_name_it_belongs_under menu_name: the_menu_name_we_want_it_in weight: -1

And that’s it. If you specify a parent link which is in a menu, you no longer even need to specify the menu name. So clearing the cache will get this menu link created and added to your menu. And even more, removing this code will remove your menu link from the menu. With D7 you need another update hook to clear that link.

Second, you can do far more powerful things than this. In the example above, we know the route name and have hardcoded it there. But what if we don’t yet and have to grab it from somewhere dynamically. That is where plugin derivatives come into play. For more information about what these are and how they work, do check out my previous article on the matter.

So let’s see an example of how we can define menu links dynamically. First, let’s head back to our *.links.menu.yml file and add our derivative declaration and then explain what we are doing:

my_module.product_link: class: Drupal\my_module\Plugin\Menu\ProductMenuLink deriver: Drupal\my_module\Plugin\Derivative\ProductMenuLink menu_name: product

First of all, we want to create dynamically a menu link inside the product menu for all the products on our site. Let’s say those are entities.

There are two main things we need to define for our dynamic menu links: the class they use and the deriver class responsible for creating a menu link derivative for each product. Additionally, we can add here in the YAML file all the static information that will be common for all these links. In this case, the menu name they’ll be in is the same for all we might as well just add it here.

Next, we need to write those two classes. The first would typically go in the Plugin/Menu namespace of our module and can look as simple as this:

namespace Drupal\my_module\Plugin\Menu; use Drupal\Core\Menu\MenuLinkDefault; /** * Represents a menu link for a single Product. */ class ProductMenuLink extends MenuLinkDefault {}

We don’t even need to have any specific functionality in our class if we don’t need it. We can extend the MenuLinkDefault class which will contain all that is needed for the default interaction with menu links — and more important, implement the MenuLinkInterface which is required. But if we need to work with these programatically a lot, we can add some helper methods to access plugin information.

Next, we can write our deriver class that goes in the Plugin/Derivative namespace of our module:

<?php namespace Drupal\my_module\Plugin\Derivative; use Drupal\Component\Plugin\Derivative\DeriverBase; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Derivative class that provides the menu links for the Products. */ class ProductMenuLink extends DeriverBase implements ContainerDeriverInterface { /** * @var EntityTypeManagerInterface $entityTypeManager. */ protected $entityTypeManager; /** * Creates a ProductMenuLink instance. * * @param $base_plugin_id * @param EntityTypeManagerInterface $entity_type_manager */ public function __construct($base_plugin_id, EntityTypeManagerInterface $entity_type_manager) { $this->entityTypeManager = $entity_type_manager; } /** * {@inheritdoc} */ public static function create(ContainerInterface $container, $base_plugin_id) { return new static( $base_plugin_id, $container->get('entity_type.manager') ); } /** * {@inheritdoc} */ public function getDerivativeDefinitions($base_plugin_definition) { $links = []; // We assume we don't have too many... $products = $this->entityTypeManager->getStorage('product')->loadMultiple(); foreach ($products as $id => $product) { $links[$id] = [ 'title' => $product->label(), 'route_name' => $product->toUrl()->getRouteName(), 'route_parameters' => ['product' => $product->id()] ] + $base_plugin_definition; } return $links; } }

This is where most of the logic happens. First, we implement the ContainerDeriverInterface so that we can expose this class to the container and inject the Entity Type Manager. You can see the create() method signature is a bit different than you are used to. Second, we implement the getDerivativeDefinitions() method to return an array of plugin definitions based on the master definition (the one found in the YAML file). To this end, we load all our products and create the array of definitions.

Some things to note about this array of definitions. The keys of this array are the ID of the derivative, which in our case will match the Product IDs. However, the menu link IDs themselves will be made up of the following construct [my_module].product_link:[product-id]. That is the name of the link we set in the YAML file + the derivative ID, separated by a colon.

The route name we add to the derivative is the canonical route of the product entity. And because this route is dynamic (has arguments) we absolutely must also have the route_parameters key where we add the necessary parameters for building this route. Had the route been static, no route params would have been necessary.

Finally, each definition is made up of what we specify here + the base plugin definition for the link (which actually includes also all the things we added in the YAML file). If we need to interact programatically with these links and read some basic information about the products themselves, we can use the options key and store that data. This can then be read by helper methods in the Drupal\my_module\Plugin\Menu\ProductMenuLink class.

And that’s it. Now if we clear the cache, all our products are in the menu. If we create another product, it’s getting added to the menu (once the caches are cleared).

Bonus

You know how you can define action links and local tasks (tabs) in the same way as menu link? In their respective YAML files? Well the same applies for the derivatives. So using this same technique, you can define local actions and tasks dynamically. The difference is that you will have a different class to extend for representing the links. For local tasks it is LocalTaskDefault and for local actions it is LocalActionDefault.

Summary

In this article we saw how we can dynamically create menu links in Drupal 8 using derivatives. In doing so, we also got a brief refresher on how derivatives work. This is a very powerful subsystem of the Plugin API which hides a lot of powerful functionality. You just gotta dig it out and use it.

Categories: Blogs

heykarthikwithu: Drupal 7 - Apache Solr Search, How to setup and how to index?

June 5, 2017 - 4:55am
Drupal 7 - Apache Solr Search, How to setup and how to index?

Install Solr on the machine, Setup the Core, Install and Configure the Apache Solr Search module and do the Indexing..

heykarthikwithu Mon, 06/05/2017 - 13:25
Categories: Blogs

Erik Erskine: Nicer date ranges in Drupal – part 3

June 4, 2017 - 8:00pm

This is the last part of a series on improving the way date ranges are presented in Drupal, by creating a field formatter that can omit the day, month or year where appropriate, displaying the date ranges in a nicer, more compact form:

  • 24–25 January 2017
  • 29 January–3 February 2017
  • 9:00am–4:30pm, 1 April 2017

In this post we look at adding an administrative interface, so site builders can add and edit formats from Drupal's UI.

Read more

Categories: Blogs

OSTraining: Creating Printer-friendly Versions of Drupal Articles

June 4, 2017 - 8:00pm

In this tutorial, we'll show you how to add a "Printer-friendly version" button to your Drupal articles. The main reason you'd want to do this is a courtesy for your readers. Many still print things they read online and you don't want them to waste that expensive printer ink just to print your logo and theme as well as the article.

This is a themed tutorial because our sister post "Creating Printer-friendly Versions of Wordpress Posts" with Wordpress tutorial covers the same topic.

Without this solution, you'd likely need to create a separate CSS file with styles specifically for the printed page.  Fortunately, the "Printer, email and PDF versions" Drupal community module makes this much easier. It will automatically create a printer-friendly version of each article.

Categories: Blogs

Matt Glaman: Swapping Drupal 8 services to customize Drupal Commerce

June 4, 2017 - 1:11pm

One of the reasons that I love Drupal 8 is the fact it is object orientated and uses the Dependency Injection pattern with a centralized service container. If you’re new to the concept, here’s some links for some fun reading.

But for now the basics are: Things define their dependencies, and a centralized thing is able to give you an object instance with all of those dependencies provided. You don’t need to manually construct a class and provide its dependencies (constructor arguments.)

This also means we do not have to use concrete classes! That means you can modify the class used for a service without ripping apart other code. Yay for being decoupled(ish)!

Why is this cool?

So that’s great, and all. But let’s actually use a real example to show how AWESOME this is. In Drupal Commerce we have the commerce_cart.cart_session service. This is how we know if an anonymous user has a cart or not. We assume this service will implement the \Drupal\commerce_cart\CartSessionInterface interface, which means we don’t care how you tell us, just tell us via our agreed methods.

The default class uses the native session handling. But we’re going to swap that out and use cookies instead. Why? Because skipping the session will preserve page cache while browsing the site catalogs and product pages.

Let’s do it

Let’s kick it off by creating a module called commerce_cart_cookies. This will swap out the existing commerce_cart.cart_session service to use our own implementation which relies on cookies instead of the PHP session.

The obvious: we need a commerce_cart_cookies.info.yml

    name: Commerce Cart Cookies
    description: Uses cookies for cart session instead of PHP sessions
    core: 8.x
    type: module
    dependencies:
    - commerce_cart

Now we need to create our class which will replace the default session handling. I’m not going to go into what the entire code would look like to satisfy the class, but the generic class would resemble the following. You can find a repo for this project at the end of the article.

    <?php

namespace Drupal\commerce_cart_cookies;

use Drupal\commerce_cart\CartSessionInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
* Uses cookies to track active carts.
*
* We inject the request stack to handle cookies within the Request object,
* and not directly.
*/
class CookieCartSession implements CartSessionInterface {

  /**
   * The current request.
   *
   * \Symfony\Component\HttpFoundation\Request
   */
  protected $request;

  /**
   * Creates a new CookieCartSession object.
   *
   * \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   */
  public function __construct(RequestStack $request_stack) {
    $this->request = $request_stack->getCurrentRequest();
    }

    /**
    * {}
    */
    public function getCartIds($type = self::ACTIVE) {
    // TODO: Implement getCartIds() method.
    }

    /**
    * {}
    */
    public function addCartId($cart_id, $type = self::ACTIVE) {
    }

    /**
    * {}
    */
    public function hasCartId($cart_id, $type = self::ACTIVE) {
    // TODO: Implement hasCartId() method.
    }

    /**
    * {}
    */
    public function deleteCartId($cart_id, $type = self::ACTIVE) {
    // TODO: Implement hasCartId() method.
    }

    }

Next we’re going to make our service provider class. This is a bit magical, as we do not actually register it anywhere. It just needs to exist. Drupal will look for classes that end in ServiceProvider within all enabled modules. Based on the implementation you can add or alter services registered in the service container when it is being compiled (which is why the process is called rebuild! not just cache clear in Drupal 8.) The class must also start with a camel cased version of your module name. So our class will be CommerceCartCookiesServiceProvider.

Create a src directory in your module and a CommerceCartCookiesServiceProvider.php file within it. Let’s scaffold out the bare minimum for our class.

    <?php

namespace Drupal\commerce_cart_cookies;

use Drupal\Core\DependencyInjection\ServiceProviderBase;

class CommerceCartCookiesServiceProvider extends ServiceProviderBase { }

Luckily for us all, core provides \Drupal\Core\DependencyInjection\ServiceProviderBase for us. This base class implements ServiceProviderInterface and ServiceModifierInterface to make it easier for us to modify the container. Let’s override the alter method so we can prepare to modify the commerce_cart.cart_session service.

        <?php

namespace Drupal\commerce_cart_cookies;

use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderBase;

class CommerceCartCookiesServiceProvider extends ServiceProviderBase {

  /**
   * {}
   */
  public function alter(ContainerBuilder $container) {
    if ($container->hasDefinition('commerce_cart.cart_session')) {
        $container->getDefinition('commerce_cart.cart_session')
        ->setClass(CookieCartSession::class)
        ->setArguments([new Reference('request_stack')]);
        }
        }

        }
   

We update the definition for commerce_cart.cart_session to use our class name, and also change it’s arguments to reflect our dependency on the request stack. The default service injects the session handler, whereas we need the request stack so we can retrieve cookies off of the current request.

The cart session service will now use our provided when the container is rebuilt!

The project code can be found at https://github.com/mglaman/commerce_cart_cookies

Categories: Blogs

Pages