<?php

namespace Drupal\jsonld\ContextGenerator;

use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\rdf\RdfMappingInterface;
use Drupal\rdf\Entity\RdfMapping;
use Psr\Log\LoggerInterface;

/**
 * A reliable JSON-LD @Context generation class.
 *
 * Class ContextGenerator.
 *
 * @package Drupal\jsonld\ContextGenerator
 */
class JsonldContextGenerator implements JsonldContextGeneratorInterface {

  /**
   * Constant Naming convention used to prefix name cache bins($cid)
   */
  const CACHE_BASE_CID = 'jsonld:context';

  /**
   * Constant hook alter name.
   */
  const FIELD_MAPPPINGS_HOOK = 'jsonld_field_mappings';

  /**
   * Injected EntityFieldManager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager = NULL;

  /**
   * Injected EntityTypeManager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager = NULL;

  /**
   * Injected EntityTypeBundle.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
   */
  protected $bundleInfo = NULL;

  /**
   * Injected Cache implementation.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * Injected Logger Interface.
   *
   * @var \Psr\Log\LoggerInterface
   *   A logger instance.
   */
  protected $logger;

  /**
   * Cached field type mappings.
   *
   * @var array
   */
  protected $fieldMappings = [];

  /**
   * Constructs a ContextGenerator object.
   *
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity manager.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
   *   The language manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
   *   The Entity Type Manager.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
   *   Caching Backend.
   * @param \Psr\Log\LoggerInterface $logger_channel
   *   Our Logging Channel.
   */
  public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeBundleInfoInterface $bundle_info, EntityTypeManagerInterface $entity_manager, CacheBackendInterface $cache_backend, LoggerInterface $logger_channel) {
    $this->entityFieldManager = $entity_field_manager;
    $this->entityTypeManager = $entity_manager;
    $this->bundleInfo = $bundle_info;
    $this->cache = $cache_backend;
    $this->logger = $logger_channel;
  }

  /**
   * {@inheritdoc}
   */
  public function getContext($ids) {
    $cid = JsonldContextGenerator::CACHE_BASE_CID . $ids;
    $cache = $this->cache->get($cid);
    $data = '';
    if (!$cache) {
      $rdfMapping = RdfMapping::load($ids);
      // Our whole chain of exceptions will never happen
      // because RdfMapping:load returns NULL on non existance
      // Which forces me to check for it
      // and don't even call writeCache on missing
      // Solution, throw also one here.
      if ($rdfMapping) {
        $data = $this->writeCache($rdfMapping, $cid);
      }
      else {
        $msg = t("Can't generate JSON-LD Context for @ids without RDF Mapping present.",
          ['@ids' => $ids]);
        $this->logger->warning("@msg",
          [
            '@msg' => $msg,
          ]);
        throw new \Exception($msg);
      }
    }
    else {
      $data = $cache->data;
    }
    return $data;
  }

  /**
   * {@inheritdoc}
   */
  public function generateContext(RdfMappingInterface $rdfMapping) {
    // TODO: we will need to use \Drupal\Core\Field\FieldDefinitionInterface
    // a lot to be able to create/frame/discern drupal bundles based on JSON-LD
    // So keep an eye on that definition.
    $allRdfNameSpaces = rdf_get_namespaces();

    // This one will become our return value.
    $jsonLdContextArray['@context'] = [];

    // Temporary array to keep track of our used namespaces and props.
    $theAccumulator = [];

    $bundle_rdf_mappings = $rdfMapping->getPreparedBundleMapping();
    $drupal_types = $this->entityBundleIdsSplitter($rdfMapping->id());
    $entity_type_id = $drupal_types['entityTypeId'];
    $bundle = $drupal_types['bundleId'];
    // If we don't have rdf:type(s) for this bundle then it makes little
    // sense to continue.
    // This only generates an Exception if there is an
    // rdfmapping object but has no rdf:type.
    if (empty($bundle_rdf_mappings['types'])) {
      $msg = t("Can't generate JSON-LD Context without at least one rdf:type for Entity type @entity_type, Bundle @bundle_name combo.",
        ['@entity_type' => $entity_type_id, ' @bundle_name' => $bundle]);
      $this->logger->warning("@msg",
        [
          '@msg' => $msg,
        ]);
      throw new \Exception($msg);
    }

    /* We have a lot of assumptions here (rdf module is strange)
    a) xsd and other utility namespaces are in place
    b) the user knows what/how rdf mapping works and does it right
    c) that if a field's mapping_type is "rel" or "rev" and datatype is
    not defined, then '@type' is uncertain.
    d) that mapping back and forward is 1 to 1.
    Drupal allows multiple fields to be mapped to a same rdf prop
    but that does not scale back. If drupal gets an input with a list
    of values for a given property, we would never know in which Drupal
    fields we should put those values. it's the many to one,
    one to many reduction problem made worst by the abstraction of
    fields being containers of mappings and not rdf properties. */
    // Only care for those mappings that point to bundled or base fields.
    // First our bundled fields.
    foreach ($this->entityFieldManager->getFieldDefinitions($entity_type_id, $bundle) as $bundleFieldName => $fieldDefinition) {
      $field_context = $this->getFieldsRdf($rdfMapping, $bundleFieldName, $fieldDefinition, $allRdfNameSpaces);
      $theAccumulator = array_merge($field_context, $theAccumulator);
    }
    // And then our Base fields.
    foreach ($this->entityFieldManager->getBaseFieldDefinitions($entity_type_id) as $baseFieldName => $fieldDefinition) {
      $field_context = $this->getFieldsRdf($rdfMapping, $baseFieldName, $fieldDefinition, $allRdfNameSpaces);
      $theAccumulator = array_merge($field_context, $theAccumulator);
    }
    $theAccumulator = array_filter($theAccumulator);
    $jsonLdContextArray['@context'] = $theAccumulator;
    return json_encode($jsonLdContextArray, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
  }

  /**
   * {@inheritdoc}
   */
  public function getFieldsRdf(RdfMappingInterface $rdfMapping, $field_name, FieldDefinitionInterface $fieldDefinition, array $allRdfNameSpaces) {
    $termDefinition = [];
    $fieldContextFragment = [];
    $fieldRDFMapping = $rdfMapping->getPreparedFieldMapping($field_name);
    if (!empty($fieldRDFMapping)) {
      // If one ore more properties, all will share same datatype so
      // get that before iterating.
      // First get our defaults, no-user or config based input.
      $default_field_term_mapping = $this->getTermContextFromField($fieldDefinition->getType());

      // Now we start overriding from config entity defined mappings.
      // Assume all non defined mapping types as "property".
      $reltype = isset($fieldRDFMapping['mapping_type']) ? $fieldRDFMapping['mapping_type'] : 'property';

      if (isset($fieldRDFMapping['datatype']) && ($reltype == 'property')) {
        $termDefinition = ['@type' => $fieldRDFMapping['datatype']];
      }
      if (!isset($fieldRDFMapping['datatype']) && ($reltype != 'property')) {
        $termDefinition = ['@type' => '@id'];
      }

      // This should respect user provided mapping and fill rest with defaults.
      $termDefinition = $termDefinition + $default_field_term_mapping;

      // Now iterate over all properties for this field
      // trying to parse them as compact IRI.
      foreach ($fieldRDFMapping['properties'] as $property) {
        $compactedDefinition = $this->parseCompactedIri($property);
        if ($compactedDefinition['prefix'] != NULL) {
          // Check if the namespace prefix exists.
          if (array_key_exists($compactedDefinition['prefix'], $allRdfNameSpaces)) {
            // Just overwrite as many times as needed,
            // still faster than checking if
            // it's there in the first place.
            $fieldContextFragment[$compactedDefinition['prefix']] = $allRdfNameSpaces[$compactedDefinition['prefix']];
            $fieldContextFragment[$property] = $termDefinition;
          }
        }
      }
    }

    return $fieldContextFragment;
  }

  /**
   * Writes JSON-LD @context cache per Entity_type bundle combo.
   *
   * @param \Drupal\rdf\RdfMappingInterface $rdfMapping
   *   Rdf mapping object.
   * @param string $cid
   *   Name of the cache bin to use.
   *
   * @return string
   *   A json encoded string for the processed JSON-LD @context
   */
  protected function writeCache(RdfMappingInterface $rdfMapping, $cid) {

    // This is how an empty json encoded @context looks like.
    $data = json_encode(['@context' => ''], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    try {
      $data = $this->generateContext($rdfMapping);
      $this->cache->set($cid, $data, Cache::PERMANENT, $rdfMapping->getCacheTagsToInvalidate());
    }
    catch (\Exception $e) {
      $this->logger->warning("@msg",
        [
          '@msg' => $e->getMessage(),
        ]);
    }

    return $data;
  }

  /**
   * Absurdly simple exploder for a joint entityType and Bundle ids string.
   *
   * @param string $ids
   *   A string with containing entity id and bundle joined by a dot.
   *
   * @return array
   *   And array with the entity type and the bundle id
   */
  protected function entityBundleIdsSplitter($ids) {
    list($entity_type_id, $bundle_id) = explode(".", $ids, 2);
    return ['entityTypeId' => $entity_type_id, 'bundleId' => $bundle_id];
  }

  /**
   * Parses and IRI, checks if it is complaint with compacted IRI definition.
   *
   * Assumes this notion of compact IRI/similar to CURIE
   * http://json-ld.org/spec/ED/json-ld-syntax/20120522/#dfn-prefix.
   *
   * @param string $iri
   *   IRIs are strings.
   *
   * @return array
   *   If $iri is a compacted iri, prefix and term as separate
   *    array members, if not, unmodified $iri in term position
   *    and null prefix.
   */
  protected function parseCompactedIri($iri) {
    // As naive as it gets.
    list($prefix, $rest) = array_pad(explode(":", $iri, 2), 2, '');
    if ((substr($rest, 0, 2) == "//") || ($prefix == $iri)) {
      // Means this was never a compacted IRI.
      return ['prefix' => NULL, 'term' => $iri];
    }
    return ['prefix' => $prefix, 'term' => $rest];
  }

  /**
   * Naive approach on Drupal field to JSON-LD type mapping.
   *
   * TODO: Would be fine to have this definitions in an
   * configEntity way in the future.
   *
   * @param string $field_type
   *   As provided by \Drupal\Core\Field\FieldDefinitionInterface::getType().
   *
   * @return array
   *   A json-ld term definition if there is a match
   *    or array("@type" => "xsd:string") in case of no match.
   */
  protected function getTermContextFromField($field_type) {
    // Be aware that drupal field definitions can be complex.
    // e.g text_with_summary has a text, a summary, a number of lines, etc
    // we are only dealing with the resulting ->value() of all this separate
    // pieces and mapping only that as a whole.
    // Default mapping to return in case no $field_type matches
    // field_mappings array keys.
    $default_mapping = [
      "@type" => "xsd:string",
    ];

    // Only load mappings from hooks if we haven't done it
    // yet for this instance.
    if (empty($this->fieldMappings)) {
      // Cribbed from rdf module's rdf_get_namespaces.
      foreach (\Drupal::moduleHandler()->getImplementations(self::FIELD_MAPPPINGS_HOOK) as $module) {
        $function = $module . '_' . self::FIELD_MAPPPINGS_HOOK;
        foreach ($function() as $field => $mapping) {
          if (array_key_exists($field, $this->fieldMappings)) {
            $this->logger->warning(
              t('Tried to map @field_type to @new_type, but @field_type is already mapped to @orig_type.', [
                '@field_type' => $field,
                '@new_type' => $mapping['@type'],
                '@orig_type' => $this->fieldMappings[$field]['@type'],
              ])
            );
          }
          else {
            $this->fieldMappings[$field] = $mapping;
          }
        }
      }
    }
    return array_key_exists($field_type, $this->fieldMappings) ? $this->fieldMappings[$field_type] : $default_mapping;
  }

}


<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL v3 or later
 *
 */
namespace Matomo\Cache;

use Matomo\Cache\Backend;

/**
 * This cache uses one "cache" entry for all cache entries it contains.
 *
 * This comes handy for things that you need very often, nearly in every request. Instead of having to read eg.
 * a hundred caches from file we only load one file which contains the hundred cache ids. Should be used only for things
 * that you need very often and only for cache entries that are not too large to keep loading and parsing the single
 * cache entry fast.
 *
 * $cache = new Eager($backend, $storageId = 'eagercache');
 * // $cache->fetch('my'id')
 * // $cache->save('myid', 'test');
 *
 * // ... at some point or at the end of the request
 * $cache->persistCacheIfNeeded($lifeTime = 43200);
 */
class Eager implements Cache
{
    /**
     * @var Backend
     */
    private $storage;
    private $storageId;
    private $content = array();
    private $isDirty = false;

    /**
     * Loads the cache entries from the given backend using the given storageId.
     *
     * @param Backend $storage
     * @param $storageId
     */
    public function __construct(Backend $storage, $storageId)
    {
        $this->storage = $storage;
        $this->storageId = $storageId;

        $content = $storage->doFetch($storageId);

        if (is_array($content)) {
            $this->content = $content;
        }
    }

    /**
     * Fetches an entry from the cache.
     *
     * Make sure to call the method {@link contains()} to verify whether there is actually any content saved under
     * this cache id.
     *
     * @param string $id The cache id.
     * @return int|float|string|boolean|array
     */
    public function fetch($id)
    {
        return $this->content[$id];
    }

    /**
     * {@inheritdoc}
     */
    public function contains($id)
    {
        return array_key_exists($id, $this->content);
    }

    /**
     * Puts data into the cache.
     *
     * @param string $id The cache id.
     * @param int|float|string|boolean|array $content
     * @param int $lifeTime Setting a lifetime is not supported by this cache and the parameter will be ignored.
     * @return boolean
     */
    public function save($id, $content, $lifeTime = 0)
    {
        if (is_object($content)) {
            throw new \InvalidArgumentException('You cannot use this cache to cache an object, only arrays, strings and numbers. Have a look at Transient cache.');
            // for performance reasons we do currently not recursively search whether any array contains an object.
        }

        $this->content[$id] = $content;
        $this->isDirty = true;
        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function delete($id)
    {
        if ($this->contains($id)) {
            $this->isDirty = true;
            unset($this->content[$id]);
            return true;
        }

        return false;
    }

    /**
     * {@inheritdoc}
     */
    public function flushAll()
    {
        $this->storage->doDelete($this->storageId);

        $this->content = array();
        $this->isDirty = false;

        return true;
    }

    /**
     * Will persist all previously made changes if there were any.
     *
     * @param int $lifeTime  The cache lifetime in seconds.
     *                       If != 0, sets a specific lifetime for this cache entry (0 => infinite lifeTime).
     */
    public function persistCacheIfNeeded($lifeTime)
    {
        if ($this->isDirty) {
            $this->storage->doSave($this->storageId, $this->content, $lifeTime);
        }
    }

}


<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL v3 or later
 *
 */
namespace Matomo\Cache\Backend;

use Matomo\Cache\Backend;

class ArrayCache implements Backend
{
    private $data = array();

    /**
     * {@inheritdoc}
     */
    public function doFetch($id)
    {
        return $this->doContains($id) ? $this->data[$id] : false;
    }

    /**
     * {@inheritdoc}
     */
    public function doContains($id)
    {
        // isset() is required for performance optimizations, to avoid unnecessary function calls to array_key_exists.
        return isset($this->data[$id]) || array_key_exists($id, $this->data);
    }

    /**
     * {@inheritdoc}
     */
    public function doSave($id, $data, $lifeTime = 0)
    {
        $this->data[$id] = $data;

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function doDelete($id)
    {
        unset($this->data[$id]);

        return true;
    }

    public function doFlush()
    {
        $this->data = array();

        return true;
    }

}


<?php

namespace Drupal\jsonld\Controller;

use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Controller\ControllerBase;
use Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Drupal\rdf\Entity\RdfMapping;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;

/**
 * Class JsonldContextController.
 *
 * @package Drupal\jsonld\Controller
 */
class JsonldContextController extends ControllerBase {

  /**
   * Injected JsonldContextGenerator.
   *
   * @var \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface
   */
  private $jsonldContextGenerator;

  /**
   * JsonldContextController constructor.
   *
   * @param \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface $jsonld_context_generator
   *   Injected JsonldContextGenerator.
   */
  public function __construct(JsonldContextGeneratorInterface $jsonld_context_generator) {
    $this->jsonldContextGenerator = $jsonld_context_generator;
  }

  /**
   * Controller's create method for dependecy injection.
   *
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The App Container.
   *
   * @return static
   *   An instance of our jsonld.contextgenerator service.
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('jsonld.contextgenerator'));
  }

  /**
   * Returns an JSON-LD Context for a entity bundle.
   *
   * @param string $entity_type
   *   Route argument, an entity type.
   * @param string $bundle
   *   Route argument, a bundle.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The Symfony Http Request.
   *
   * @return \Symfony\Component\HttpFoundation\Response
   *   An Http response.
   */
  public function content($entity_type, $bundle, Request $request) {

    // TODO: expose cached/not cached through
    // more varied HTTP response codes.
    try {
      $context = $this->jsonldContextGenerator->getContext("$entity_type.$bundle");
      $response = new CacheableJsonResponse(json_decode($context), 200);
      $response->setEncodingOptions(JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);
      $response->headers->set('Content-Type', 'application/ld+json');

      // For now deal with Cache dependencies manually.
      $meta = new CacheableMetadata();
      $meta->setCacheContexts(['user.permissions', 'ip', 'url']);
      $meta->setCacheTags(RdfMapping::load("$entity_type.$bundle")->getCacheTags());
      $meta->setCacheMaxAge(Cache::PERMANENT);
      $response->addCacheableDependency($meta);
    }
    catch (\Exception $e) {
      $response = new Response($e->getMessage(), 400);
    }

    return $response;
  }

}


<?php

namespace Drupal\jsonld\Normalizer;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\FieldItemListInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;

/**
 * Converts the Drupal field structure to JSON-LD array structure.
 */
class FieldNormalizer extends NormalizerBase {

  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\Core\Field\FieldItemListInterface';

  /**
   * {@inheritdoc}
   */
  public function normalize($field, $format = NULL, array $context = []) {

    $normalized_field_items = [];

    // Get the field definition.
    $entity = $field->getEntity();
    $field_name = $field->getName();
    $field_definition = $field->getFieldDefinition();

    // If this field is not translatable, it can simply be normalized without
    // separating it into different translations.
    if (!$field_definition->isTranslatable()) {
      $normalized_field_items = $this->normalizeFieldItems($field, $format, $context);
    }
    // Otherwise, the languages have to be extracted from the entity and passed
    // in to the field item normalizer in the context. The langcode is appended
    // to the field item values.
    else {
      foreach ($entity->getTranslationLanguages() as $language) {
        $context['langcode'] = $language->getId();
        $translation = $entity->getTranslation($language->getId());
        $translated_field = $translation->get($field_name);
        $normalized_field_items = array_merge($normalized_field_items, $this->normalizeFieldItems($translated_field, $format, $context));
      }
    }

    $normalized = NestedArray::mergeDeepArray($normalized_field_items);
    // I'm really not sure if this is the best approach
    // but in my defense, it works.
    if (!isset($normalized['@graph'])) {
      $normalized_in_context['@graph'][$context['current_entity_id']] = $normalized;
    }
    else {
      $normalized_in_context = $normalized;
    }
    return $normalized_in_context;
  }

  /**
   * {@inheritdoc}
   */
  public function denormalize($data, $class, $format = NULL, array $context = []) {

    if (!isset($context['target_instance'])) {
      throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldNormalizer');
    }
    if ($context['target_instance']->getParent() == NULL) {
      throw new InvalidArgumentException('The field passed in via $context[\'target_instance\'] must have a parent set.');
    }

    $items = $context['target_instance'];
    $item_class = $items->getItemDefinition()->getClass();
    foreach ($data as $item_data) {
      // Create a new item and pass it as the target for the unserialization of
      // $item_data. Note: if $item_data is about a different language than the
      // default, FieldItemNormalizer::denormalize() will dismiss this item and
      // create a new one for the right language.
      $context['target_instance'] = $items->appendItem();
      $this->serializer->denormalize($item_data, $item_class, $format, $context);
    }

    return $items;

  }

  /**
   * Helper function to normalize field items.
   *
   * @param \Drupal\Core\Field\FieldItemListInterface $field
   *   The field object.
   * @param string $format
   *   The format.
   * @param array $context
   *   The context array.
   *
   * @return array
   *   The array of normalized field items.
   */
  protected function normalizeFieldItems(FieldItemListInterface $field, $format, array $context) {

    $normalized_field_items = [];
    if (!$field->isEmpty()) {
      foreach ($field as $field_item) {
        $normalized_field_items[] = $this->serializer->normalize($field_item, $format, $context);
      }
    }
    return $normalized_field_items;
  }

}


<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL v3 or later
 *
 */
namespace Matomo\Cache\Backend;

use Matomo\Cache\Backend;

/**
 * TODO: extend Doctrine ChainCache as soon as available
 */
class Chained implements Backend
{
    /**
     * @var Backend[]
     */
    private $backends = array();

    /**
     * Initializes the chained backend.
     *
     * @param Backend[] $backends An array of backends to use. They should be ordered from fastest to slowest.
     */
    public function __construct($backends = array())
    {
        $this->backends = array_values($backends);
    }

    public function getBackends()
    {
        return $this->backends;
    }

    public function doFetch($id)
    {
        foreach ($this->backends as $key => $backend) {
            if ($backend->doContains($id)) {
                $value = $backend->doFetch($id);

                // EG If chain is ARRAY => REDIS => DB and we find result in DB we will update REDIS and ARRAY
                for ($subKey = $key - 1 ; $subKey >= 0 ; $subKey--) {
                    $this->backends[$subKey]->doSave($id, $value, 300); // TODO we should use the actual TTL here
                }

                return $value;
            }
        }

        return false;
    }

    public function doContains($id)
    {
        foreach ($this->backends as $backend) {
            if ($backend->doContains($id)) {
                return true;
            }
        }

        return false;
    }

    public function doSave($id, $data, $lifeTime = 0)
    {
        $stored = true;

        foreach ($this->backends as $backend) {
            $stored = $backend->doSave($id, $data, $lifeTime) && $stored;
        }

        return $stored;
    }

    // returns true even when file does not exist
    public function doDelete($id)
    {
        foreach ($this->backends as $backend) {
            if ($backend->doContains($id)) {
                $backend->doDelete($id);
            }
        }

        return true;
    }

    public function doFlush()
    {
        $flushed = true;

        foreach ($this->backends as $backend) {
            $flushed = $backend->doFlush() && $flushed;
        }

        return $flushed;
    }

}


<?php

namespace Drupal\jsonld\Normalizer;

use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\hal\LinkManager\LinkManagerInterface;
use Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface;
use Drupal\serialization\EntityResolver\EntityResolverInterface;
use Drupal\serialization\EntityResolver\UuidReferenceInterface;

/**
 * Converts the Drupal entity reference item object to JSON-LD array structure.
 */
class EntityReferenceItemNormalizer extends FieldItemNormalizer implements UuidReferenceInterface {

  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';

  /**
   * The hypermedia link manager.
   *
   * @var \Drupal\hal\LinkManager\LinkManagerInterface
   */
  protected $linkManager;

  /**
   * The entity resolver.
   *
   * @var \Drupal\serialization\EntityResolver\EntityResolverInterface
   */
  protected $entityResolver;

  /**
   * Constructs an EntityReferenceItemNormalizer object.
   *
   * @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
   *   The hypermedia link manager.
   * @param \Drupal\serialization\EntityResolver\EntityResolverInterface $entity_Resolver
   *   The entity resolver.
   * @param \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface $jsonld_context
   *   The Json-Ld context service.
   */
  public function __construct(LinkManagerInterface $link_manager,
  EntityResolverInterface $entity_Resolver,
                              JsonldContextGeneratorInterface $jsonld_context) {
    parent::__construct($jsonld_context);
    $this->linkManager = $link_manager;
    $this->entityResolver = $entity_Resolver;
  }

  /**
   * {@inheritdoc}
   */
  public function normalize($field_item, $format = NULL, array $context = []) {

    /* @var $field_item \Drupal\Core\Field\FieldItemInterface */
    $target_entity = $field_item->get('entity')->getValue();
    $normalized_prop = [];
    // If this is not a content entity, let the parent implementation handle it,
    // only content entities are supported as embedded resources.
    if (!($target_entity instanceof FieldableEntityInterface)) {
      return parent::normalize($field_item, $format, $context);
    }
    // If the parent entity passed in a langcode, unset it before normalizing
    // the target entity. Otherwise, untranslatable fields of the target entity
    // will include the langcode.
    $langcode = isset($context['langcode']) ? $context['langcode'] : NULL;
    unset($context['langcode']);
    // Limiting to uuid makes sure that we only get one child from base entity
    // if not we could end traversing forever since there is no way
    // we can enforce acyclic entity references.
    $context['included_fields'] = ['uuid'];
    $context['needs_jsonldcontext'] = FALSE;
    $context['embedded'] = TRUE;
    // Normalize the target entity.
    // This will call \Drupal\jsonld\Normalizer\ContentEntityNormalizer.
    $embedded = $this->serializer->normalize($target_entity, $format, $context);

    if (isset($context['current_entity_rdf_mapping'])) {
      $values_clean = [];
      // So why i am passing the whole rdf mapping object and not
      // only the predicate? Well because i hope i will be able
      // to MAP to RDF also sub fields of a complex field someday
      // and somehow.
      $field_mappings = $context['current_entity_rdf_mapping']->getPreparedFieldMapping(
        $field_item->getParent()
          ->getName()
      );
      $field_keys = isset($field_mappings['properties']) ?
            $field_mappings['properties'] :
            [$field_item->getParent()->getName()];
      if (!empty($field_mappings['datatype'])) {
        $values_clean['@type'] = $field_mappings['datatype'];
      }

      // Value in this case is the target entity, so if a callback exists
      // it should work against that?
      if (!empty($field_mappings['datatype_callback'])) {
        $callback = $field_mappings['datatype_callback']['callable'];
        $arguments = isset($field_mappings['datatype_callback']['arguments']) ? $field_mappings['datatype_callback']['arguments'] : NULL;
        $values_clean['@value'] = call_user_func($callback, $target_entity, $arguments);
      }
      // Since getting the to embed entity URL here could be a little bit
      // expensive and would require an helper method
      // i could just borrow it from the $embed result.
      $values_clean['@id'] = key($embedded['@graph']);
      // Because having a @type for a reference causes problems, we strip that.
      // This could be removed using headers in future.
      if (isset($values_clean['@type'])) {
        unset($values_clean['@type']);
      }

      // The returned structure will be recursively merged into the normalized
      // JSON-LD @Graph.
      foreach ($field_keys as $field_name) {
        // If there's no context, we need full predicates, not shortened ones.
        if (!$context['needs_jsonldcontext']) {
          $field_name = $this->escapePrefix($field_name, $context['namespaces']);
          foreach ($values_clean as $key => $val) {
            // Expand values in the array, ie. @type values xsd:string.
            $values_clean[$key] = $this->escapePrefix($val, $context['namespaces']);
          }
        }
        $normalized_prop[$field_name] = [$values_clean];
      }

    }

    $normalized_in_context = array_merge_recursive($embedded, ['@graph' => [$context['current_entity_id'] => $normalized_prop]]);

    return $normalized_in_context;
  }

  /**
   * {@inheritdoc}
   */
  protected function constructValue(array $data, array $context) {

    $field_item = $context['target_instance'];
    $field_definition = $field_item->getFieldDefinition();
    $target_type = $field_definition->getSetting('target_type');
    $id = $this->entityResolver->resolve($this, $data, $target_type);
    if (isset($id)) {
      return ['target_id' => $id];
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function getUuid($data) {

    if (isset($data['uuid'])) {
      $uuid = $data['uuid'];
      // The value may be a nested array like $uuid[0]['value'].
      if (is_array($uuid) && isset($uuid[0]['value'])) {
        $uuid = $uuid[0]['value'];
      }
      return $uuid;
    }
  }

}


<?php

namespace Drupal\jsonld\Normalizer;

use Drupal\Core\Field\FieldItemInterface;
use Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;

/**
 * Converts the Drupal field item object structure to JSON-LD array structure.
 */
class FieldItemNormalizer extends NormalizerBase {

  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\Core\Field\FieldItemInterface';

  /**
   * Json-Ld context generator service.
   *
   * @var \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface
   */
  protected $jsonldContextgenerator;

  /**
   * FieldItemNormalizer constructor.
   *
   * @param \Drupal\jsonld\ContextGenerator\JsonldContextGeneratorInterface $jsonld_context
   *   Json-Ld context generator service.
   */
  public function __construct(JsonldContextGeneratorInterface $jsonld_context) {
    $this->jsonldContextgenerator = $jsonld_context;
  }

  /**
   * {@inheritdoc}
   */
  public function normalize($field_item, $format = NULL, array $context = []) {

    // @TODO Understand Drupal complex fields to RDF mapping
    // Fields can be complex, with multiple subfields
    // but i'm not sure if rdf module
    // is able to assign that, so investigate:
    $values = $field_item->toArray();
    // For now we will just pass @value and @language to json-ld
    // Until we find a way of mapping to rdf subfields.
    $values_clean = [];
    $normalized = [];
    $field = $field_item->getParent();

    if (!isset($values['value']) && !isset($values['uri'])) {
      // Makes little sense to add to json-ld without a value or uri.
      return [];
    }
    else {
      // Set the uri here, and then convert it to '@id' later on.
      $values_clean['@value'] = isset($values['value']) ? $values['value'] : $values['uri'];
      if (isset($context['current_entity_rdf_mapping'])) {
        // So why i am passing the whole rdf mapping object and not
        // only the predicate? Well because i hope i will be able
        // to MAP to RDF also sub fields of a complex field someday
        // and somehow.
        $field_mappings = $context['current_entity_rdf_mapping']->getPreparedFieldMapping($field->getName());
        $field_keys = isset($field_mappings['properties']) ? $field_mappings['properties'] : [$field->getName()];

        if (!empty($field_mappings['datatype'])) {
          $values_clean['@type'] = $field_mappings['datatype'];
        }

        // Well, this is json-ld but $field_item->toArray() depends on
        // each field implementation and some don't have 'value key'
        // Maybe i should handle them as _blank nodes?
        // For now this is a dirty solution.
        if (!empty($field_mappings['datatype_callback'])) {
          $callback = $field_mappings['datatype_callback']['callable'];
          $arguments = isset($field_mappings['datatype_callback']['arguments']) ? $field_mappings['datatype_callback']['arguments'] : NULL;
          $values_clean['@value'] = call_user_func($callback, $values, $arguments);
        }
        $field_context = $this->jsonldContextgenerator->getFieldsRdf(
          $context['current_entity_rdf_mapping'],
          $field->getName(),
          $field->getFieldDefinition(),
          $context['namespaces']
        );

        if (isset($field_context[$field_keys[0]])) {
          $values_clean = $values_clean + $field_context[$field_keys[0]];
        }

      }
      else {
        $field_keys = [$field->getName()];
      }

      // JSON-LD Spec says you can't have an @language for a typed values.
      if (isset($context['langcode']) &&
      (!isset($values_clean['@type']) ||
      empty($values_clean['@type']) ||
      $values_clean['@type'] == "xsd:string" ||
      $values_clean['@type'] == "http://www.w3.org/2001/XMLSchema#string")) {
        // @see https://json-ld.org/spec/latest/json-ld/#dfn-strings.
        // Getting rid of @type to allow @language.
        // @see https://json-ld.org/spec/latest/json-ld/#string-internationalization.
        unset($values_clean['@type']);
        $values_clean['@language'] = $context['langcode'];
      }

      array_filter($values_clean);

      // Convert @value to @id if the item we're normalizing is a Link.
      if (isset($values['uri'])) {
        $values_clean['@id'] = $values_clean['@value'];
        unset($values_clean['@value']);
        unset($values_clean['@type']);
      }

      // The values are wrapped in an array, and then wrapped in another array
      // keyed by field name so that field items can be merged by the
      // FieldNormalizer.
      foreach ($field_keys as $field_name) {
        // If there's no context, we need full predicates, not shortened ones.
        if (!$context['needs_jsonldcontext']) {
          $field_name = $this->escapePrefix($field_name, $context['namespaces']);
          foreach ($values_clean as $key => $val) {
            // Expand values in the array, ie. @type values xsd:string.
            $values_clean[$key] = $this->escapePrefix($val, $context['namespaces']);
          }
        }
        $normalized[$field_name] = [$values_clean];
      }
      return $normalized;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function denormalize($data, $class, $format = NULL, array $context = []) {

    if (!isset($context['target_instance'])) {
      throw new InvalidArgumentException('$context[\'target_instance\'] must be set to denormalize with the FieldItemNormalizer');
    }
    if ($context['target_instance']->getParent() == NULL) {
      throw new InvalidArgumentException('The field item passed in via $context[\'target_instance\'] must have a parent set.');
    }

    $field_item = $context['target_instance'];

    // If this field is translatable, we need to create a translated instance.
    if (isset($data['lang'])) {
      $langcode = $data['lang'];
      unset($data['lang']);
      $field_definition = $field_item->getFieldDefinition();
      if ($field_definition->isTranslatable()) {
        $field_item = $this->createTranslatedInstance($field_item, $langcode);
      }
    }

    $field_item->setValue($this->constructValue($data, $context));
    return $field_item;
  }

  /**
   * Build the field item value using the incoming data.
   *
   * @param array $data
   *   The incoming data for this field item.
   * @param array $context
   *   The context passed into the Normalizer.
   *
   * @return mixed
   *   The value to use in Entity::setValue().
   */
  protected function constructValue(array $data, array $context) {

    return $data;
  }

  /**
   * Get a translated version of the field item instance.
   *
   * To indicate that a field item applies to one translation of an entity and
   * not another, the property path must originate with a translation of the
   * entity. This is the reason for using target_instances, from which the
   * property path can be traversed up to the root.
   *
   * @param \Drupal\Core\Field\FieldItemInterface $item
   *   The untranslated field item instance.
   * @param string $langcode
   *   The langcode.
   *
   * @return \Drupal\Core\Field\FieldItemInterface
   *   The translated field item instance.
   */
  protected function createTranslatedInstance(FieldItemInterface $item, $langcode) {

    // Remove the untranslated item that was created for the default language
    // by FieldNormalizer::denormalize().
    $items = $item->getParent();
    $delta = $item->getName();
    unset($items[$delta]);

    // Instead, create a new item for the entity in the requested language.
    $entity = $item->getEntity();
    $entity_translation = $entity->hasTranslation($langcode) ? $entity->getTranslation($langcode) : $entity->addTranslation($langcode);
    $field_name = $item->getFieldDefinition()->getName();
    return $entity_translation->get($field_name)->appendItem();
  }

}


<?php

namespace Drupal\jsonld\Normalizer;

use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\hal\LinkManager\LinkManagerInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;

/**
 * Converts the Drupal entity object structure to a JSON-LD array structure.
 */
class ContentEntityNormalizer extends NormalizerBase {

  const NORMALIZE_ALTER_HOOK = "jsonld_alter_normalized_array";

  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\Core\Entity\ContentEntityInterface';

  /**
   * The hypermedia link manager.
   *
   * @var \Drupal\hal\LinkManager\LinkManagerInterface
   */
  protected $linkManager;

  /**
   * The entity manager.
   *
   * @var \Drupal\Core\Entity\EntityManagerInterface
   */
  protected $entityManager;

  /**
   * The module handler.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * Constructs an ContentEntityNormalizer object.
   *
   * @param \Drupal\hal\LinkManager\LinkManagerInterface $link_manager
   *   The hypermedia link manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_manager
   *   The entity manager.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler.
   */
  public function __construct(LinkManagerInterface $link_manager, EntityTypeManagerInterface $entity_manager, ModuleHandlerInterface $module_handler) {

    $this->linkManager = $link_manager;
    $this->entityManager = $entity_manager;
    $this->moduleHandler = $module_handler;
  }

  /**
   * {@inheritdoc}
   */
  public function normalize($entity, $format = NULL, array $context = []) {

    // We need to make sure that this only runs for JSON-LD.
    // @TODO check $format before going RDF crazy
    $normalized = [];

    if (isset($context['depth'])) {
      $context['depth'] += 1;
    }

    $context += [
      'account' => NULL,
      'included_fields' => NULL,
      'needs_jsonldcontext' => FALSE,
      'embedded' => FALSE,
      'namespaces' => rdf_get_namespaces(),
      'depth' => 0,
    ];

    if ($context['needs_jsonldcontext']) {
      $normalized['@context'] = $context['namespaces'];
    }
    // Let's see if this content entity has
    // rdf mapping associated to the bundle.
    $rdf_mappings = rdf_get_mapping($entity->getEntityTypeId(), $entity->bundle());
    $bundle_rdf_mappings = $rdf_mappings->getPreparedBundleMapping();

    // In Drupal space, the entity type URL.
    $drupal_entity_type = $this->linkManager->getTypeUri($entity->getEntityTypeId(), $entity->bundle(), $context);

    // Extract rdf:types.
    $hasTypes = empty($bundle_rdf_mappings['types']);
    $types = $hasTypes ? $drupal_entity_type : $bundle_rdf_mappings['types'];

    // If there's no context and the types are not drupal
    // entity types, we need full predicates,
    // not shortened ones. So we replace them in place.
    if ($context['needs_jsonldcontext'] === FALSE && is_array($types)) {
      for ($i = 0; $i < count($types); $i++) {
        $types[$i] = ContentEntityNormalizer::escapePrefix($types[$i], $context['namespaces']);
      }
    }

    // Create the array of normalized fields, starting with the URI.
    /* @var $entity \Drupal\Core\Entity\ContentEntityInterface */
    $normalized = $normalized + [
      '@graph' => [
        $this->getEntityUri($entity) => [
          '@id' => $this->getEntityUri($entity),
          '@type' => $types,
        ],
      ],
    ];

    // If the fields to use were specified, only output those field values.
    // We could make use of this context key
    // To limit json-ld output to an subset
    // that is just compatible with fcrepo4 and LDP?
    if (isset($context['included_fields'])) {
      $fields = [];
      foreach ($context['included_fields'] as $field_name) {
        $fields[] = $entity->get($field_name);
      }
    }
    else {
      $fields = $entity->getFields();
    }

    $context['current_entity_id'] = $this->getEntityUri($entity);
    $context['current_entity_rdf_mapping'] = $rdf_mappings;

    foreach ($fields as $name => $field) {
      // Just process fields that have rdf mappings defined.
      // We could also pass as not contextualized keys the others
      // if needed.
      if (!empty($rdf_mappings->getPreparedFieldMapping($name))) {
        // Continue if the current user does not have access to view this field.
        if (!$field->access('view', $context['account'])) {
          continue;
        }
        // This tells consecutive calls to content entity normalisers
        // that @context is not needed again.
        $normalized_property = $this->serializer->normalize($field, $format, $context);
        // $this->serializer in questions does implement normalize
        // but the interface (typehint) does not.
        // We could check if serializer implements normalizer interface
        // to avoid any possible errors in case someone swaps serializer.
        $normalized = array_merge_recursive($normalized, $normalized_property);
      }
    }
    // Clean up @graph if this is the top-level entity
    // by converting from associative to numeric indexed.
    if (!$context['embedded']) {
      $normalized['@graph'] = array_values($normalized['@graph']);
    }

    if (isset($context['depth']) && $context['depth'] == 0) {
      $this->moduleHandler->invokeAll(self::NORMALIZE_ALTER_HOOK,
        [$entity, &$normalized, $context]
      );
    }
    return $normalized;
  }

  /**
   * {@inheritdoc}
   */
  public function denormalize($data, $class, $format = NULL, array $context = []) {

    // Get type, necessary for determining which bundle to create.
    if (!isset($data['_links']['type'])) {
      throw new UnexpectedValueException('The type link relation must be specified.');
    }

    // Create the entity.
    $typed_data_ids = $this->getTypedDataIds($data['_links']['type'], $context);
    $entity_type = $this->entityManager->getDefinition($typed_data_ids['entity_type']);
    $langcode_key = $entity_type->getKey('langcode');
    $values = [];

    // Figure out the language to use.
    if (isset($data[$langcode_key])) {
      $values[$langcode_key] = $data[$langcode_key][0]['value'];
      // Remove the langcode so it does not get iterated over below.
      unset($data[$langcode_key]);
    }

    if ($entity_type->hasKey('bundle')) {
      $bundle_key = $entity_type->getKey('bundle');
      $values[$bundle_key] = $typed_data_ids['bundle'];
      // Unset the bundle key from data, if it's there.
      unset($data[$bundle_key]);
    }

    $entity = $this->entityManager->getStorage($typed_data_ids['entity_type'])->create($values);

    // Remove links from data array.
    unset($data['_links']);
    // Get embedded resources and remove from data array.
    $embedded = [];
    if (isset($data['_embedded'])) {
      $embedded = $data['_embedded'];
      unset($data['_embedded']);
    }

    // Flatten the embedded values.
    foreach ($embedded as $relation => $field) {
      $field_ids = $this->linkManager->getRelationInternalIds($relation);
      if (!empty($field_ids)) {
        $field_name = $field_ids['field_name'];
        $data[$field_name] = $field;
      }
    }

    // Pass the names of the fields whose values can be merged.
    $entity->_restSubmittedFields = array_keys($data);

    // Iterate through remaining items in data array. These should all
    // correspond to fields.
    foreach ($data as $field_name => $field_data) {
      $items = $entity->get($field_name);
      // Remove any values that were set as a part of entity creation (e.g
      // uuid). If the incoming field data is set to an empty array, this will
      // also have the effect of emptying the field in REST module.
      $items->setValue([]);
      if ($field_data) {
        // Denormalize the field data into the FieldItemList object.
        $context['target_instance'] = $items;
        $this->serializer->denormalize($field_data, get_class($items), $format, $context);
      }
    }

    return $entity;
  }

  /**
   * Constructs the entity URI.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity.
   *
   * @return string
   *   The entity URI.
   */
  protected function getEntityUri(EntityInterface $entity) {

    // Some entity types don't provide a canonical link template, at least call
    // out to ->url().
    if ($entity->isNew() || !$entity->hasLinkTemplate('canonical')) {
      return $entity->toUrl('canonical', []);
    }
    $url = $entity->toUrl('canonical', ['absolute' => TRUE]);
    return $url->setRouteParameter('_format', 'jsonld')->toString();
  }

  /**
   * Gets the typed data IDs for a type URI.
   *
   * @param array $types
   *   The type array(s) (value of the 'type' attribute of the incoming data).
   * @param array $context
   *   Context from the normalizer/serializer operation.
   *
   * @return array
   *   The typed data IDs.
   */
  protected function getTypedDataIds(array $types, array $context = []) {

    // The 'type' can potentially contain an array of type objects. By default,
    // Drupal only uses a single type in serializing, but allows for multiple
    // types when deserializing.
    if (isset($types['href'])) {
      $types = [$types];
    }

    foreach ($types as $type) {
      if (!isset($type['href'])) {
        throw new UnexpectedValueException('Type must contain an \'href\' attribute.');
      }
      $type_uri = $type['href'];
      // Check whether the URI corresponds to a known type on this site. Break
      // once one does.
      if ($typed_data_ids = $this->linkManager->getTypeInternalIds($type['href'], $context)) {
        break;
      }
    }

    // If none of the URIs correspond to an entity type on this site, no entity
    // can be created. Throw an exception.
    if (empty($typed_data_ids)) {
      throw new UnexpectedValueException(sprintf('Type %s does not correspond to an entity on this site.', $type_uri));
    }

    return $typed_data_ids;
  }

}


<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL v3 or later
 *
 */
namespace Matomo\Cache\Backend;

use Matomo\Cache\Backend;

class Factory
{
    public function buildArrayCache()
    {
        return new ArrayCache();
    }

    public function buildFileCache($options)
    {
        return new File($options['directory']);
    }

    public function buildNullCache()
    {
        return new NullCache();
    }

    public function buildChainedCache($options)
    {
        $backends = array();

        foreach ($options['backends'] as $backendToBuild) {

            $backendOptions = array();
            if (array_key_exists($backendToBuild, $options)) {
                $backendOptions = $options[$backendToBuild];
            }

            $backends[] = $this->buildBackend($backendToBuild, $backendOptions);
        }

        return new Chained($backends);
    }

    public function buildRedisCache($options)
    {
        if (empty($options['unix_socket']) && (empty($options['host']) || empty($options['port']))) {
            throw new \InvalidArgumentException('RedisCache is not configured. Please provide at least a host and a port');
        }

        $timeout = 0.0;
        if (array_key_exists('timeout', $options)) {
            $timeout = $options['timeout'];
        }

        $redis = new \Redis();
        if (empty($options['unix_socket'])) {
            $redis->connect($options['host'], $options['port'], $timeout);
        } else {
            $redis->connect($options['unix_socket'], null, $timeout);
        }

        if (!empty($options['password'])) {
            $redis->auth($options['password']);
        }

        if (array_key_exists('database', $options)) {
            $redis->select((int) $options['database']);
        }

        $redisCache = new Redis();
        $redisCache->setRedis($redis);

        return $redisCache;
    }

    /**
     * Build a specific backend instance.
     *
     * @param string $type The type of backend you want to create. Eg 'array', 'file', 'chained', 'null', 'redis'.
     * @param array $options An array of options for the backend you want to create.
     * @return Backend
     * @throws Factory\BackendNotFoundException In case the given type was not found.
     */
    public function buildBackend($type, array $options)
    {
        switch ($type) {
            case 'array':

                return $this->buildArrayCache();

            case 'file':

                return $this->buildFileCache($options);

            case 'chained':

                return $this->buildChainedCache($options);

            case 'null':

                return $this->buildNullCache();

            case 'redis':

                return $this->buildRedisCache($options);

            default:

                throw new Factory\BackendNotFoundException("Cache backend $type not valid");
        }
    }
}


<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL v3 or later
 *
 */
namespace Matomo\Cache;

use Matomo\Cache\Backend;

class Lazy implements Cache
{
    private $backend;

    /**
     * Initializes the cache.
     *
     * @param Backend $backend Any backend that should be used to store / hold the cache entries.
     */
    public function __construct(Backend $backend)
    {
        $this->backend = $backend;
    }

    /**
     * Fetches an entry from the cache.
     *
     * @param string $id The cache id.
     * @return mixed The cached data or FALSE, if no cache entry exists for the given id.
     */
    public function fetch($id)
    {
        $id = $this->getCompletedCacheIdIfValid($id);

        return $this->backend->doFetch($id);
    }

    /**
     * {@inheritdoc}
     */
    public function contains($id)
    {
        $id = $this->getCompletedCacheIdIfValid($id);

        return $this->backend->doContains($id);
    }

    /**
     * Puts data into the cache.
     *
     * @param string $id       The cache id.
     * @param mixed  $data     The cache entry/data.
     * @param int    $lifeTime The cache lifetime in seconds.
     *                         If != 0, sets a specific lifetime for this cache entry (0 => infinite lifeTime).
     *
     * @return boolean TRUE if the entry was successfully stored in the cache, FALSE otherwise.
     */
    public function save($id, $data, $lifeTime = 0)
    {
        $id = $this->getCompletedCacheIdIfValid($id);

        if (is_object($data)) {
            throw new \InvalidArgumentException('You cannot use this cache to cache an object, only arrays, strings and numbers. Have a look at Transient cache.');
            // for performance reasons we do currently not recursively search whether any array contains an object.
        }

        return $this->backend->doSave($id, $data, $lifeTime);
    }

    /**
     * {@inheritdoc}
     */
    public function delete($id)
    {
        $id = $this->getCompletedCacheIdIfValid($id);

        return $this->backend->doDelete($id);
    }

    /**
     * {@inheritdoc}
     */
    public function flushAll()
    {
        return $this->backend->doFlush();
    }

    private function getCompletedCacheIdIfValid($id)
    {
        $this->checkId($id);
        return 'matomocache_' . $id;
    }

    private function checkId($id)
    {
        if (empty($id)) {
            throw new \InvalidArgumentException('Empty cache id given');
        }

        if (!$this->isValidId($id)) {
            throw new \InvalidArgumentException("Invalid cache id request $id");
        }
    }

    /**
     * Returns true if the string is a valid id.
     *
     * Id that start with a-Z or 0-9 and contain a-Z, 0-9, underscore(_), dash(-), and dot(.) will be accepted.
     * Id beginning with anything but a-Z or 0-9 will be rejected (including .htaccess for example).
     * Id containing anything other than above mentioned will also be rejected (file names with spaces won't be accepted).
     *
     * @param string $id
     * @return bool
     */
    private function isValidId($id)
    {
        return (0 !== preg_match('/(^[a-zA-Z0-9]+([a-zA-Z_0-9.-]*))$/D', $id));
    }
}


<?php

/**
 * TechDivision\Import\Attribute\Observers\CatalogAttributeUpdateObserver
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 *
 * PHP version 5
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */

namespace TechDivision\Import\Attribute\Observers;

use TechDivision\Import\Attribute\Utils\MemberNames;

/**
 * Observer that add/update's the EAV catalog attribute itself.
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */
class CatalogAttributeUpdateObserver extends CatalogAttributeObserver
{

    /**
     * Initialize the attribute with the passed attributes and returns an instance.
     *
     * @param array $attr The attribute attributes
     *
     * @return array The initialized attribute
     */
    protected function initializeAttribute(array $attr)
    {

        // try to load the EAV catalog attribute with the attribute code
        if ($attribute = $this->loadCatalogAttribute($attr[MemberNames::ATTRIBUTE_ID])) {
            // unserialize the additional data value if available
            if (is_array($attribute[MemberNames::ADDITIONAL_DATA]) && $attribute[MemberNames::ADDITIONAL_DATA] !== null) {
                // merge the additional data if available
                $attribute[MemberNames::ADDITIONAL_DATA] = array_merge(
                    $attr[MemberNames::ADDITIONAL_DATA],
                    json_decode($attribute[MemberNames::ADDITIONAL_DATA])
                );
            } elseif (!is_array($attribute[MemberNames::ADDITIONAL_DATA]) && $attribute[MemberNames::ADDITIONAL_DATA] !== null) {
                // unserialize and override the additional data
                $attribute[MemberNames::ADDITIONAL_DATA] = json_decode($attribute[MemberNames::ADDITIONAL_DATA]);
            } else {
                // nothing here
            }

            // merge the attributes into the entity
            return $this->serializeAdditionalData($this->mergeEntity($attribute, $attr));
        }

        //  serialize the additional data and return the attributes
        return parent::initializeAttribute($attr);
    }

    /**
     * Load's and return's the EAV catalog attribute with the passed attribute ID.
     *
     * @param string $attributeId The ID of the EAV catalog attribute to load
     *
     * @return array The EAV catalog attribute
     */
    protected function loadCatalogAttribute($attributeId)
    {
        return $this->getAttributeBunchProcessor()->loadCatalogAttribute($attributeId);
    }
}


<?php

namespace StatonLab\TripalTestSuite\Services;

use StatonLab\TripalTestSuite\Exceptions\FunctionNotFoundException;
use PHPUnit\Framework\Assert as PHPUnit;

class SilentResponse extends BaseResponse
{
    /**
     * Callable's printed output.
     *
     * @var string
     */
    protected $content;

    /**
     * Callable return value.
     *
     * @var mixed
     */
    protected $return;

    /**
     * SilentResponse constructor.
     *
     * @param string|callable $callable
     * @param array $arguments Optional. Array of arguments if callable is a string.
     * @throws \StatonLab\TripalTestSuite\Exceptions\FunctionNotFoundException
     */
    public function __construct($callable, array $arguments = [])
    {
        if (is_string($callable)) {
            return $this->runString($callable, $arguments);
        }

        return $this->runCallable($callable);
    }

    /**
     * Runs a user function.
     *
     * @param string $callable Function name.
     * @param array $arguments Arguments to pass to the user function.
     * @return $this
     * @throws \StatonLab\TripalTestSuite\Exceptions\FunctionNotFoundException
     */
    protected function runString($callable, array $arguments)
    {
        if (! function_exists($callable)) {
            throw new FunctionNotFoundException("Function $callable not found");
        }

        $this->startBuffer();

        $this->return = call_user_func_array($callable, $arguments);

        $this->endBuffer();

        return $this;
    }

    /**
     * Runs a `callable` function such as anonymous functions or invokable classes.
     *
     * @param callable $callable
     * @return $this
     */
    protected function runCallable(callable $callable)
    {
        $this->startBuffer();

        $this->return = $callable();

        $this->endBuffer();

        return $this;
    }

    /**
     * Starts the output buffer collection.
     */
    protected function startBuffer()
    {
        $this->content = '';

        ob_start(function ($buffer) {
            $this->content = $buffer;
        });

        putenv('TRIPAL_SUPPRESS_ERRORS=TRUE');
    }

    /**
     * Alias to getContent.
     *
     * @return string
     */
    public function getResponseBody()
    {
        return $this->getContent();
    }

    /**
     * Ends and cleans the buffer.
     */
    protected function endBuffer()
    {
        putenv('TRIPAL_SUPPRESS_ERRORS');
        ob_end_clean();
    }

    /**
     * Returns the printed output as a string.
     *
     * @return string
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * @return mixed
     */
    public function getReturnValue()
    {
        return $this->return;
    }

    /**
     * Assert the return value of the callable equals the given value.
     *
     * @param $value
     *
     * @return $this
     */
    public function assertReturnEquals($value)
    {
        PHPUnit::assertEquals($value, $this->getReturnValue());

        return $this;
    }

    /**
     * Clean up memory.
     */
    public function __destruct()
    {
        $this->content = null;
        $this->return = null;
    }
}


<?php

/**
 * TechDivision\Import\Attribute\Observers\AttributeOptionExportObserver
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 *
 * PHP version 5
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */

namespace TechDivision\Import\Attribute\Observers;

use TechDivision\Import\Attribute\Utils\ColumnKeys;

/**
 * Observer that exports the attribute options to an additional CSV file for further processing.
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */
class AttributeOptionExportObserver extends AbstractAttributeExportObserver
{

    /**
     * The artefact type.
     *
     * @var string
     */
    const ARTEFACT_TYPE = 'option-import';

    /**
     * Process the observer's business logic.
     *
     * @return void
     */
    protected function process()
    {

        // do NOT export the value if we're NOT in the admin store view
        if (!$this->isAdminStore()) {
            return;
        }

        // initialize the array with the artefacts
        $artefacts = array();

        // load the attribute option values/positions
        $attributeOptionValues = $this->getValue(ColumnKeys::ATTRIBUTE_OPTION_VALUES, array(), function ($value) {
            return $this->explode($value, $this->getMultipleFieldDelimiter());
        });
        $attributeOptionSortOrder = $this->getValue(ColumnKeys::ATTRIBUTE_OPTION_SORT_ORDER, array(), function ($value) {
            return $this->explode($value, $this->getMultipleFieldDelimiter());
        });
        $attributeOptionSwatch = $this->getValue(ColumnKeys::ATTRIBUTE_OPTION_SWATCH, array(), function ($value) {
            return $this->explode($value, $this->getMultipleValueDelimiter());
        });

        // iterate over the attribute option values and export them
        foreach ($attributeOptionValues as $key => $attributeOptionValue) {
            // initialize the attribute option swatch data
            $optionSwatch = array();
            if (isset($attributeOptionSwatch[$key])) {
                foreach ($this->explode($attributeOptionSwatch[$key]) as $value) {
                    $explodedSwatch = $this->explode($value, '=');
                    if (isset($explodedSwatch[0]) && isset($explodedSwatch[1])) {
                        $optionSwatch[$explodedSwatch[0]] = $explodedSwatch[1];
                    }
                }
            }

            // initialize and add the new artefact
            $artefacts[] = $this->newArtefact(
                array(
                    ColumnKeys::ATTRIBUTE_CODE  => $this->getValue(ColumnKeys::ATTRIBUTE_CODE),
                    ColumnKeys::VALUE           => $attributeOptionValue,
                    ColumnKeys::DEFAULT_VALUE   => $this->getValue(ColumnKeys::DEFAULT_VALUE),
                    ColumnKeys::SORT_ORDER      => isset($attributeOptionSortOrder[$key]) ? $attributeOptionSortOrder[$key] : 0,
                    ColumnKeys::SWATCH_TYPE     => isset($optionSwatch[ColumnKeys::TYPE]) ? $optionSwatch[ColumnKeys::TYPE] : null,
                    ColumnKeys::SWATCH_VALUE    => isset($optionSwatch[ColumnKeys::VALUE]) ? $optionSwatch[ColumnKeys::VALUE] : null
                ),
                array(
                    ColumnKeys::ATTRIBUTE_CODE  => ColumnKeys::ATTRIBUTE_CODE,
                    ColumnKeys::VALUE           => ColumnKeys::VALUE,
                    ColumnKeys::DEFAULT_VALUE   => ColumnKeys::DEFAULT_VALUE,
                    ColumnKeys::SORT_ORDER      => ColumnKeys::ATTRIBUTE_OPTION_SORT_ORDER,
                    ColumnKeys::SWATCH_TYPE     => ColumnKeys::ATTRIBUTE_OPTION_SWATCH,
                    ColumnKeys::SWATCH_VALUE    => ColumnKeys::ATTRIBUTE_OPTION_SWATCH
                )
            );
        }

        // add the array with the artefacts
        $this->addArtefacts($artefacts);
    }

    /**
     * Return's the artefact type used for export.
     *
     * @return string The artefact type
     */
    protected function getArtefactType()
    {
        return AttributeOptionExportObserver::ARTEFACT_TYPE;
    }
}


<?php

/**
 * TechDivision\Import\Attribute\Observers\AttributeOptionObserver
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 *
 * PHP version 5
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */

namespace TechDivision\Import\Attribute\Observers;

use TechDivision\Import\Attribute\Utils\ColumnKeys;
use TechDivision\Import\Attribute\Utils\MemberNames;
use TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface;

/**
 * Observer that create's the attribute options found in the additional CSV file.
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */
class AttributeOptionObserver extends AbstractAttributeImportObserver
{

    /**
     * The attribute processor instance.
     *
     * @var \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface
     */
    protected $attributeBunchProcessor;

    /**
     * Initializes the observer with the passed subject instance.
     *
     * @param \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface $attributeBunchProcessor The attribute bunch processor instance
     */
    public function __construct(AttributeBunchProcessorInterface $attributeBunchProcessor)
    {
        $this->attributeBunchProcessor = $attributeBunchProcessor;
    }

    /**
     * Process the observer's business logic.
     *
     * @return void
     */
    protected function process()
    {

        // query whether or not, we've found a new attribute code => means we've found a new attribute
        if ($this->hasBeenProcessed($this->getValue(ColumnKeys::ATTRIBUTE_CODE), $this->getValue(ColumnKeys::VALUE))) {
            return;
        }

        // prepare the store view code
        $this->prepareStoreViewCode();

        // prepare the attribue values
        $attributeOption = $this->initializeAttribute($this->prepareAttributes());

        // insert the attribute option and set the option ID
        $this->setLastOptionId($this->persistAttributeOption($attributeOption));
    }

    /**
     * Prepare the attributes of the entity that has to be persisted.
     *
     * @return array The prepared attributes
     */
    protected function prepareAttributes()
    {

        // load the attribute ID
        $attribute = $this->loadAttributeByEntityTypeIdAndAttributeCode($this->getEntityTypeId(), $this->getValue(ColumnKeys::ATTRIBUTE_CODE));
        $attributeId = $attribute[MemberNames::ATTRIBUTE_ID];

        // load the sort order
        $sortOrder = $this->getValue(ColumnKeys::SORT_ORDER);

        // return the prepared attribute option
        return $this->initializeEntity(
            array(
                MemberNames::ATTRIBUTE_ID  => $attributeId,
                MemberNames::SORT_ORDER    => $sortOrder
            )
        );
    }

    /**
     * Initialize the EAV attribute option with the passed attributes and returns an instance.
     *
     * @param array $attr The EAV attribute option attributes
     *
     * @return array The initialized EAV attribute option
     */
    protected function initializeAttribute(array $attr)
    {
        return $attr;
    }

    /**
     * Return's the attribute bunch processor instance.
     *
     * @return \TechDivision\Import\Attribute\Services\AttributeBunchProcessorInterface The attribute bunch processor instance
     */
    protected function getAttributeBunchProcessor()
    {
        return $this->attributeBunchProcessor;
    }

    /**
     * Queries whether or not the attribute with the passed code/value has already been processed.
     *
     * @param string $attributeCode The attribute code to check
     * @param string $value         The option value to check
     *
     * @return boolean TRUE if the path has been processed, else FALSE
     */
    protected function hasBeenProcessed($attributeCode, $value)
    {
        return $this->getSubject()->hasBeenProcessed($attributeCode, $value);
    }

    /**
     * Set's the ID of the option that has been created recently.
     *
     * @param integer $lastOptionId The option ID
     *
     * @return void
     */
    protected function setLastOptionId($lastOptionId)
    {
        $this->getSubject()->setLastOptionId($lastOptionId);
    }

    /**
     * Return's the EAV attribute with the passed entity type ID and code.
     *
     * @param integer $entityTypeId  The entity type ID of the EAV attribute to return
     * @param string  $attributeCode The code of the EAV attribute to return
     *
     * @return array The EAV attribute
     */
    public function loadAttributeByEntityTypeIdAndAttributeCode($entityTypeId, $attributeCode)
    {
        return $this->getAttributeBunchProcessor()->loadAttributeByEntityTypeIdAndAttributeCode($entityTypeId, $attributeCode);
    }

    /**
     * Persist the passed attribute option.
     *
     * @param array $attributeOption The attribute option to persist
     *
     * @return void
     */
    protected function persistAttributeOption(array $attributeOption)
    {
        return $this->getAttributeBunchProcessor()->persistAttributeOption($attributeOption);
    }
}


<?php

/**
 * TechDivision\Import\Attribute\Actions\Processors\AttributeUpdateProcessor
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 *
 * PHP version 5
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */

namespace TechDivision\Import\Attribute\Actions\Processors;

use TechDivision\Import\Attribute\Utils\MemberNames;
use TechDivision\Import\Attribute\Utils\SqlStatementKeys;
use TechDivision\Import\Actions\Processors\AbstractUpdateProcessor;

/**
 * The EAV attribute update processor implementation.
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */
class AttributeUpdateProcessor extends AbstractUpdateProcessor
{

    /**
     * Return's the array with the SQL statements that has to be prepared.
     *
     * @return array The SQL statements to be prepared
     * @see \TechDivision\Import\Actions\Processors\AbstractBaseProcessor::getStatements()
     */
    protected function getStatements()
    {

        // return the array with the SQL statements that has to be prepared
        return array(
            SqlStatementKeys::UPDATE_ATTRIBUTE => $this->loadStatement(SqlStatementKeys::UPDATE_ATTRIBUTE)
        );
    }

    /**
     * Update's the passed row.
     *
     * @param array       $row  The row to update
     * @param string|null $name The name of the prepared statement that has to be executed
     *
     * @return string The ID of the updated attribute
     */
    public function execute($row, $name = null)
    {
        parent::execute($row, $name);
        return $row[MemberNames::ATTRIBUTE_ID];
    }
}


<?php

/**
 * TechDivision\Import\Attribute\Actions\Processors\AttributeOptionUpdateProcessor
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 *
 * PHP version 5
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */

namespace TechDivision\Import\Attribute\Actions\Processors;

use TechDivision\Import\Attribute\Utils\MemberNames;
use TechDivision\Import\Attribute\Utils\SqlStatementKeys;
use TechDivision\Import\Actions\Processors\AbstractUpdateProcessor;

/**
 * The EAV attribute option update processor implementation.
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */
class AttributeOptionUpdateProcessor extends AbstractUpdateProcessor
{

    /**
     * Return's the array with the SQL statements that has to be prepared.
     *
     * @return array The SQL statements to be prepared
     * @see \TechDivision\Import\Actions\Processors\AbstractBaseProcessor::getStatements()
     */
    protected function getStatements()
    {

        // return the array with the SQL statements that has to be prepared
        return array(
            SqlStatementKeys::UPDATE_ATTRIBUTE_OPTION => $this->loadStatement(SqlStatementKeys::UPDATE_ATTRIBUTE_OPTION)
        );
    }

    /**
     * Update's the passed row.
     *
     * @param array       $row  The row to update
     * @param string|null $name The name of the prepared statement that has to be executed
     *
     * @return string The ID of the updated attribute
     */
    public function execute($row, $name = null)
    {
        parent::execute($row, $name);
        return $row[MemberNames::OPTION_ID];
    }
}


<?php

namespace StatonLab\TripalTestSuite\Console\Commands;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;

abstract class BaseCommand extends Command
{
    /**
     * @var InputInterface
     */
    private $input;

    /**
     * @var OutputInterface
     */
    private $output;

    /**
     * Begin executing the command.
     *
     * @param \Symfony\Component\Console\Input\InputInterface $input
     * @param \Symfony\Component\Console\Output\OutputInterface $output
     * @return int|null|void
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $this->input = $input;
        $this->output = $output;

        $this->handle();
    }

    /**
     * Override this function to run your script upon command call.
     */
    abstract protected function handle();

    /**
     * Write one or multiple lines.
     *
     * @param string|array $line
     */
    public function line($line)
    {
        $this->output->writeln($line);
    }

    /**
     * Write a success or info message.
     *
     * @param string|array $line
     */
    public function info($line)
    {
        if (is_array($line)) {
            $lines = [];
            foreach ($line as $one) {
                $lines[] = "<info>$one</info>";
            }

            $this->output->writeln($lines);

            return;
        }

        $this->output->writeln("<info>$line</info>");
    }

    /**
     * Write an error.
     *
     * @param string|array $line
     */
    public function error($line)
    {
        if (is_array($line)) {
            $lines = [];
            foreach ($line as $one) {
                $lines[] = "<error>$one</error>";
            }

            $this->output->writeln($lines);

            return;
        }

        $this->output->writeln("<error>$line</error>");
    }

    /**
     * Get an argument.
     *
     * @param $name
     * @return mixed
     */
    public function getArgument($name)
    {
        return $this->input->getArgument($name);
    }

    /**
     * Get all available arguments.
     *
     * @return array
     */
    public function getArguments()
    {
        return $this->input->getArguments();
    }

    /**
     * Check if an argument is present.
     *
     * @param $name
     * @return bool
     */
    public function hasArgument($name)
    {
        return $this->input->hasArgument($name);
    }

    /**
     * Get an option.
     *
     * @param $name
     * @return mixed
     */
    public function getOption($name)
    {
        return $this->input->getOption($name);
    }

    /**
     * Get all options.
     *
     * @return array
     */
    public function getOptions()
    {
        return $this->input->getOptions();
    }

    /**
     * Check if an options is present.
     *
     * @param $name
     * @return bool
     */
    public function hasOption($name)
    {
        return $this->input->hasOption($name);
    }

    /**
     * Ask the user a question.
     *
     * @param $question
     * @param bool $default
     * @return mixed
     */
    public function ask($question, $default = false)
    {
        $handler = $this->getHelper('question');
        $confirmation = new ConfirmationQuestion($question, $default);

        return $handler->ask($this->input, $this->output, $confirmation);
    }
}


<?php

namespace Tests\DatabaseSeeders;

use StatonLab\TripalTestSuite\Database\Seeder;
use \Exception;

class DevSeedSeeder extends Seeder
{
    /**
     * Part one:
     * Chado records.
     * These are records used by many of the below importers.
     * ALL importers require an organism.
     * The expression data loader will associate the data with the
     * $expression_analysis record. All other importers associate data with the
     * $sequence_analysis. Uncomment the array to create that chado record.
     */

protected $organism = [
        'common_name' => 'F. excelsior miniature',
        'genus' => 'Fraxinus',
        'species' => 'excelsior',
        'abbreviation' => 'F. excelsor',
        'comment' => 'Loaded with TripalDev Seed.',
      ];

protected $sequence_analysis = [
      'name' => 'Fraxinus exclesior miniature dataset',
      'description' => 'Tripal Dev Seed',
    ];

    protected $expression_analysis = [

      'name' => 'Fraxinus exclesior miniature dataset Expression Analysis',
      'description' => 'Tripal Dev Seed',
    ];

  protected $blastdb = [
    'name' => 'DevSeed Database: TREMBL',
    'description' => 'A dummy database created by DevSeed',
  ];

    /**
     * Part 2:
     * Files.
     * Each importer will take a file argument.  This argument should be an array
     * with one of the following two keys: file_remote => url where the file is
     * located file_local => server path where the file is located.
     */

    protected $landmark_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/sequences/empty_landmarks.fasta'];

    protected $landmark_type = 'supercontig';

    protected $mRNA_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/sequences/mrna_mini.fasta'];

    protected $protein_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/sequences/polypeptide_mini.fasta'];

    protected $gff_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/gff/filtered.gff'];

    protected $blast_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/gff/filtered.gff'];

    protected $biomaterial_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/biomaterials/biomaterials.xml'];

    protected $expression_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/expression/expression.tsv'];

    protected $interpro_file = ['file_remote' => 'https://raw.githubusercontent.com/statonlab/tripal_dev_seed/master/Fexcel_mini/ips/polypeptide_mini.fasta.xml'];

    // Regular expression that will link the protein name to the mRNA parent feature name.
   // protected $prot_regexp = '/(FRA.*?)(?=:)/';

    protected $prot_regexp = null;

    public function __construct()
    {

        if ($this->organism) {

            try {
                $organism = $this->fetch_chado_record('chado.organism', [
                    'common_name',
                    'organism_id',
                ], $this->organism);
            } catch (\Exception $e) {
                echo $e->getMessage();
                exit;
            }

            $this->organism = $organism;

            if ($this->sequence_analysis) {

                try {
                    $seq_analysis = $this->fetch_chado_record('chado.analysis', ['analysis_id'],
                        $this->sequence_analysis);
                } catch (\Exception $e) {
                    echo $e->getMessage();
                    exit;
                }
                $this->sequence_analysis = $seq_analysis;
            }

            if ($this->expression_analysis) {
                try {
                    $expression_analysis = $this->fetch_chado_record('chado.analysis', ['analysis_id'],
                        $this->expression_analysis);
                } catch (\Exception $e) {
                    echo $e->getMessage();
                    exit;
                }

                $this->expression_analysis = $expression_analysis;
            }
        }

        if ($this->blastdb) {
            try {
                $blastdb = $this->fetch_chado_record('chado.db', ['db_id'], $this->blastdb);
            } catch (\Excetion $e) {
                echo $e->getMessage();
            }

            $this->blastdb = $blastdb;
        }
    }

    /**
     * Runs all loaders.
     * Will only run loaders where the files have been uncommented at the start
     * of the class.
     */
    public function up()
    {

        if ($this->landmark_file) {

            $run_args = [
                'organism_id' => $this->organism->organism_id,
                'analysis_id' => $this->sequence_analysis->analysis_id,
                'seqtype' => $this->landmark_type,
                'method' => 2, //default insert and update
                'match_type' => 1, //unique name default
                //optional
                're_name' => null,
                're_uname' => null,
                're_accession' => null,
                'db_id' => null,
                'rel_type' => null,
                're_subject' => null,
                'parent_type' => null,
            ];
            $this->load_landmarks($run_args, $this->landmark_file);
        }

        if ($this->gff_file) {
            $run_args = [
                'analysis_id' => $this->sequence_analysis->analysis_id,
                'organism_id' => $this->organism->organism_id,
                'use_transaction' => 1,
                'add_only' => 0,
                'update' => 1,
                'create_organism' => 0,
                'create_target' => 0,

                ///regexps for mRNA and protein.
                're_mrna' => null,
                're_protein' => $this->prot_regexp,
                //optional
                'target_organism_id' => null,
                'target_type' => null,
                'start_line' => null,
                'landmark_type' => null,
                'alt_id_attr' => null,
            ];
            $this->load_GFF($run_args, $this->gff_file);
        }

        if ($this->mRNA_file) {

            $run_args = [
                'organism_id' => $this->organism->organism_id,
                'analysis_id' => $this->sequence_analysis->analysis_id,
                'seqtype' => 'mRNA',
                'method' => 2, //default insert and update
                'match_type' => 1, //unique name default
                //optional
                're_name' => null,
                're_uname' => null,
                're_accession' => null,
                'db_id' => null,
                'rel_type' => null,
                're_subject' => null,
                'parent_type' => null,
            ];
            $this->load_mRNA_FASTA($run_args, $this->mRNA_file);
        }

        if ($this->protein_file) {
            $run_args = [
                'organism_id' => $this->organism->organism_id,
                'analysis_id' => $this->sequence_analysis->analysis_id,
                'seqtype' => 'polypeptide',
                'method' => 2,
                'match_type' => 1,
                //optional
                're_name' => null,
                're_uname' => null,
                're_accession' => null,
                'db_id' => null,
            ];

            if ($this->prot_regexp) {
                //links polypeptide to mRNA
                $run_args['rel_type'] = 'derives_from';
                $run_args['re_subject'] = $this->prot_regexp;
                $run_args['parent_type'] = 'mRNA';
            }
            $this->load_polypeptide_FASTA($run_args, $this->protein_file);
        }

        if ($this->interpro_file) {

            $run_args = [
                'analysis_id' => $this->sequence_analysis->analysis_id,
                //optional
                'query_type' => 'mRNA',
                'query_re' => $this->prot_regexp,
                'query_uniquename' => null,
                'parsego' => true,
            ];

            $this->load_interpro_annotations($run_args, $this->interpro_file);
        }

        if ($this->blast_file) {
            $run_args = [
                'analysis_id' => $this->sequence_analysis->analysis_id,
                'no_parsed' => 25,//number results to parse
                'query_type' => 'mRNA',
                //optional
                'blastdb' => $this->blastdb->db_id,
                'blastfile_ext' => null,
                'is_concat' => 0,
                'query_re' => null,
                'query_uniquename' => 0,
            ];

            $this->load_blast_annotations($run_args, $this->blast_file);
        }

        if ($this->biomaterial_file) {
            $run_args = [
                'organism_id' => $this->organism->organism_id,
                'analysis_id' => $this->sequence_analysis->analysis_id,
            ];
            //optional: specifies specific CVterms for properties/property values.  Not used here.
            //'cvterm_configuration' => NULL,
            //'cvalue_configuration' => NULL];

            $this->load_biomaterials($run_args, $this->biomaterial_file);
        }

        if ($this->expression_file) {
            $run_args = [
                'filetype' => 'mat', //matrix file type
                'organism_id' => $this->organism->organism_id,
                'analysis_id' => $this->sequence_analysis->analysis_id,
                //optional
                'fileext' => null,
                're_start' => null,
                're_stop' => null,
                'feature_uniquenames' => null,
                'quantificationunits' => null,
                'seqtype' => 'mRNA',
            ];
            $this->load_expression($run_args, $this->expression_file);
        }
    }

    private function load_landmarks($run_args, $file)
    {
        module_load_include('inc', 'tripal_chado', 'includes/TripalImporter/FASTAImporter');

        $importer = new \FASTAImporter();
        $importer->create($run_args, $file);
        $importer->prepareFiles();
        $importer->run();
    }

    private function load_mRNA_FASTA($run_args, $file)
    {
        module_load_include('inc', 'tripal_chado', 'includes/TripalImporter/FASTAImporter');

        $importer = new \FASTAImporter();
        $importer->create($run_args, $file);
        $importer->prepareFiles();
        $importer->run();
    }

    private function load_polypeptide_FASTA($run_args, $file)
    {
        module_load_include('inc', 'tripal_chado', 'includes/TripalImporter/FASTAImporter');

        $importer = new \FASTAImporter();
        $importer->create($run_args, $file);
        $importer->prepareFiles();
        $importer->run();
    }

    private function load_interpro_annotations($run_args, $file)
    {
        module_load_include('inc', 'tripal_analysis_interpro', 'includes/TripalImporter/InterProImporter');

        $importer = new \InterProImporter();
        $importer->create($run_args, $file);
        $importer->prepareFiles();
        $importer->run();
    }

    private function load_GFF($run_args, $file)
    {
        module_load_include('inc', 'tripal_chado', 'includes/TripalImporter/GFF3Importer');

        $importer = new \GFF3Importer();
        $importer->create($run_args, $file);
        $importer->prepareFiles();
        $importer->run();
    }

    private function load_blast_annotations($run_args, $file)
    {
        module_load_include('inc', 'tripal_analysis_blast', 'includes/TripalImporter/BlastImporter');

        $importer = new \BlastImporter();
        $importer->create($run_args, $file);
        $importer->prepareFiles();
        $importer->run();
    }

    private function load_biomaterials($run_args, $file)
    {
        module_load_include('inc', 'tripal_biomaterial', 'includes/TripalImporter/tripal_biomaterial_loader_v3');

        $importer = new \tripal_biomaterial_loader_v3();
        $importer->create($run_args, $file);
        $importer->prepareFiles();
        $importer->run();
    }

    private function load_expression($run_args, $file)
    {
        module_load_include('inc', 'tripal_analysis_expression',
            'includes/TripalImporter/tripal_expression_data_loader');

        $importer = new \tripal_expression_data_loader();
        $importer->create($run_args, $file);
        $importer->prepareFiles();
        $importer->run();
    }

    private function fetch_chado_record($table, $fields, $factory_array)
    {
        $query = db_select($table, 't')->fields('t', $fields);

        foreach ($factory_array as $key => $value) {
            $query->condition($key, $value);
        }

        $count_query = $query;
        $count = (int) $count_query->countQuery()->execute()->fetchField();

        if ($count === 0) {
            return factory($table)->create($factory_array);
        }

        if ($count === 1) {
            return $query->execute()->fetchObject();
        }

        throw new Exception("Error creating object for: ".$table.".\n Array supplied matches ".$count_query." results, must match 1.");
    }
}


<?php

/**
 * TechDivision\Import\Attribute\Callbacks\CreateMultiselectOptionValueCallback
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 *
 * PHP version 5
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */

namespace TechDivision\Import\Attribute\Callbacks;

use TechDivision\Import\Utils\StoreViewCodes;
use TechDivision\Import\Callbacks\AbstractCallback;
use TechDivision\Import\Observers\AttributeCodeAndValueAwareObserverInterface;

/**
 * A callback implementation that creates the option values, if not exists, for the passed
 * multiselect with the passed attribute code.
 *
 * @author    Tim Wagner <t.wagner@techdivision.com>
 * @copyright 2016 TechDivision GmbH <info@techdivision.com>
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
 * @link      https://github.com/techdivision/import-attribute
 * @link      http://www.techdivision.com
 */
class CreateMultiselectOptionValueCallback extends AbstractCallback
{

    /**
     * The option value and swatch handler instance.
     *
     * @var \TechDivision\Import\Attribute\Callbacks\OptionValueAndSwatchHandlerInterface
     */
    protected $optionValueAndSwatchHandler;

    /**
     * Initialize the callback with the passed option value and swatch handler instance.
     *
     * @param \TechDivision\Import\Attribute\Callbacks\OptionValueAndSwatchHandlerInterface $optionValueAndSwatchHandler The option value and swatch handler instance
     */
    public function __construct(OptionValueAndSwatchHandlerInterface $optionValueAndSwatchHandler)
    {
        $this->optionValueAndSwatchHandler = $optionValueAndSwatchHandler;
    }

    /**
     * Returns the option value and swatch handler instance.
     *
     * @return \TechDivision\Import\Attribute\Callbacks\OptionValueAndSwatchHandlerInterface The option value and swatch handler instance
     */
    protected function getOptionValueAndSwatchHandler()
    {
        return $this->optionValueAndSwatchHandler;
    }

    /**
     * Return's the product SKU as unique identifier of the actual row.
     *
     * @return mixed The row's unique identifier
     */
    protected function getUniqueIdentifier()
    {
        return $this->getValue('sku');
    }

    /**
     * Will be invoked by a observer it has been registered for.
     *
     * @param \TechDivision\Import\Observers\ObserverInterface $observer The observer
     *
     * @return mixed The modified value
     */
    public function handle(AttributeCodeAndValueAwareObserverInterface $observer)
    {

        // set the observer
        $this->setObserver($observer);

        // load the attribute code and value
        $attributeCode = $observer->getAttributeCode();
        $attributeValue = $observer->getAttributeValue();

        // load the ID of the actual store
        $storeId = $this->getStoreId(StoreViewCodes::ADMIN);

        // explode the multiselect values
        $vals = explode('|', $attributeValue);

        // convert the option values into option value ID's
        foreach ($vals as $val) {
            $this->getOptionValueAndSwatchHandler()->createOptionValueIfNecessary($attributeCode, $storeId, $val);
        }

        // return the values
        return $attributeValue;
    }
}