code:
# Embed

[![Build Status][ico-travis]][link-travis]
[![Quality Score][ico-scrutinizer]][link-scrutinizer]
[![Reference Status][ico-references]][link-references]
[![Latest Version on Packagist][ico-version]][link-packagist]
[![Total Downloads][ico-downloads]][link-packagist]
[![Monthly Downloads][ico-m-downloads]][link-packagist]
[![Software License][ico-license]](LICENSE)
[![SensioLabs Insight][ico-sensiolabs]][link-sensiolabs]

PHP library to get information from any web page (using oembed, opengraph, twitter-cards, scrapping the html, etc). It's compatible with any web service (youtube, vimeo, flickr, instagram, etc) and has adapters to some sites like (archive.org, github, facebook, etc).

Requirements:

* PHP 5.5+
* Curl library installed

>
* If you need PHP 5.3 support, use the 1.x version
* If you need PHP 5.4 support, use the 2.x version

## Online demo

http://oscarotero.com/embed3/demo

## Installation

This package is installable and autoloadable via Composer as [embed/embed](https://packagist.org/packages/embed/embed).

```
$ composer require embed/embed
```

If you cannot (or don't want to) use composer, just include the [PSR-4 autoload.php file](src/autoloader.php) in your code.

## Usage

```php
use Embed\Embed;

//Load any url:
$info = Embed::create('https://www.youtube.com/watch?v=PP1xn5wHtxE');

//Get content info

$info->title; //The page title
$info->description; //The page description
$info->url; //The canonical url
$info->type; //The page type (link, video, image, rich)
$info->tags; //The page keywords (tags)

$info->images; //List of all images found in the page
$info->image; //The image choosen as main image
$info->imageWidth; //The width of the main image
$info->imageHeight; //The height of the main image

$info->code; //The code to embed the image, video, etc
$info->width; //The width of the embed code
$info->height; //The height of the embed code
$info->aspectRatio; //The aspect ratio (width/height)

$info->authorName; //The resource author
$info->authorUrl; //The author url

$info->providerName; //The provider name of the page (Youtube, Twitter, Instagram, etc)
$info->providerUrl; //The provider url
$info->providerIcons; //All provider icons found in the page
$info->providerIcon; //The icon choosen as main icon

$info->publishedDate; //The published date of the resource
$info->license; //The license url of the resource
$info->linkedData; //The linked-data info (http://json-ld.org/)
$info->feeds; //The RSS/Atom feeds
```

## The adapter

The adapter is the class that get all information of the page from the providers and choose the best result for each value. For example, a page can provide multiple titles from opengraph, twitter cards, oembed, the `<title>` html element, etc, so the adapter get all this titles and choose the best one.

Embed has an generic adapter called "Webpage" to use with any web but has also some specific adapters for sites like archive.org, facebook, google, github, spotify, etc, that provides information using their own apis, or need to fix any specific issue.

The available options for the adapters are:

Name | Type | Description
-----|------|------------
`min_image_width` | `int` | Minimal image width used to choose the main image
`min_image_height` | `int` | Minimal image height used to choose the main image
`choose_bigger_image` | `bool` | Choose the bigger image as the main image (instead the first found, that usually is the most relevant).
`images_blacklist` | `string`|`array` | Images that you don't want to be used. Could be plain text or [Uri](https://github.com/oscarotero/Embed/blob/master/src/Http/Url.php) match pattern.
`url_blacklist` | `string`|`array` | URLs that you don't want to be used. Could be plain text or [Uri](https://github.com/oscarotero/Embed/blob/master/src/Http/Url.php) match pattern.
`follow_canonical` | `bool` | Whether to redirect to the canonical URL or not.
`custom_adapters_namespace` | `string`|`array` | A namespace used to load custom adapters. This allows to override the behaviour of existing adapters or add support for more sites.

## The providers

The providers get the data from different sources. Each source has it's own provider. For example, there is a provider for opengraph, other for twitter cards, for oembed, html, etc. Some providers can be configured and are the following:

### oembed

Used to get data from oembed api if it's available:

Name | Type | Description
-----|------|------------
`parameters` | `array` | Extra query parameters to send with the oembed request
`embedly_key` | `string` | If it's defined, use embed.ly api as fallback oembed provider.
`iframely_key` | `string` | If it's defined, use iframe.ly api as fallback oembed provider.

### html

Used to get data directly from the html code of the page:

Name | Type | Description
-----|------|------------
`max_images` | `int` | Max number of images fetched from the html code (searching for the `<img>` elements). By default is -1 (no limit). Use 0 to no get images.
`external_images` | `bool`|`array` | By default is `false`, this means that images located in other domains are not allowed. You can set `true` (allow all) or provide an array of url patterns.

### google

Used only for google maps to generate the embed code.

Name | Type | Description
-----|------|------------
`key` | `string` | The key used [for the embed api](https://developers.google.com/maps/documentation/embed/)

### soundcloud

Used only for soundcloud pages, to get information using its api.

Name | Type | Description
-----|------|------------
`key` | `string` | The key used to get info from soundcloud API.

### facebook

Used only for facebook events (not needed for posts, images, etc), to get information using its api.

Name | Type | Description
-----|------|------------
`key` | `string` | The access token used to get info from facebook graph API.
`events_fields` | `string` | Comma-separated list of fields to query for a facebook event. Please refer to [Facebook documentation](https://developers.facebook.com/docs/graph-api/reference/event) for the full list of available fields.
`videos_fields` | `string` | Comma-separated list of fields to query for a facebook video. Please refer to [Facebook documentation](https://developers.facebook.com/docs/graph-api/reference/event) for the full list of available fields.

## Example with all options:

The options are passed as the second argument as you can see in the following example:

```php
$info = Embed::create($url, [
    'min_image_width' => 100,
    'min_image_height' => 100,
    'choose_bigger_image' => true,
    'images_blacklist' => 'example.com/*',
    'url_blacklist' => 'example.com/*',
    'follow_canonical' => true,

    'html' => [
        'max_images' => 10,
        'external_images' => true
    ],

    'oembed' => [
        'parameters' => [],
        'embedly_key' => 'YOUR_KEY',
        'iframely_key' => 'YOUR_KEY',
    ],

    'google' => [
        'key' => 'YOUR_KEY',
    ],

    'soundcloud' => [
        'key' => 'YOUR_KEY',
    ],

    'facebook' => [
        'key' => 'YOUR_KEY',
        'fields' => 'field1,field2,field3' // default : cover,description,end_time,id,name,owner,place,start_time,timezone
    ],
]);
```

## The dispatcher

To dispatch the http request, Embed includes the interface `Embed\Http\DispatcherInterface`. By default the curl library is used but you can create your own dispatcher to use any other library like [guzzle](https://github.com/guzzle/guzzle):

```php
use Embed\Embed;
use Embed\Http\DispatcherInteface;
use Embed\Http\Url;
use Embed\Http\Response;
use Embed\Http\ImageResponse;

class MyDispatcher implements DispatcherInterface
{
    public function dispatch(Url $url)
    {
        $result = function_to_execute_request($url);

        return new Response($url, $result['url'], $result['status'], $result['type'], $result['content'], $result['headers']);
    }

    public function dispatchImages(array $urls)
    {
        $responses = [];

        foreach ($urls as $url) {
            $result = function_to_get_image_size($url);

            if ($result) {
                $responses[] = new ImageResponse($url, $result['url'], $result['status'], $result['type'], $result['size'], $result['headers']);
            }
        }

        return $responses;
    }
}

//Use the dispatcher passing as third argument
$info = Embed::create('http://example.com', null, new MyDispatcher());
```

The default curl dispatcher accepts the same options that the [curl_setopt PHP function](http://php.net/manual/en/function.curl-setopt.php). You can edit the default values:

```php
use Embed\Embed;
use Embed\Http\CurlDispatcher;

$dispatcher = new CurlDispatcher([
    CURLOPT_MAXREDIRS => 20,
    CURLOPT_CONNECTTIMEOUT => 10,
    CURLOPT_TIMEOUT => 10,
    CURLOPT_SSL_VERIFYPEER => false,
    CURLOPT_SSL_VERIFYHOST => false,
    CURLOPT_ENCODING => '',
    CURLOPT_AUTOREFERER => true,
    CURLOPT_USERAGENT => 'Embed PHP Library',
    CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
]);

$info = Embed::create('http://example.com', null, $dispatcher);
```

## Accessing to advanced data

The adapter get the data from all providers and choose the best values. But you can get all available values accessing directly to each provider. There's also the possibility to inspect all http requests executed for debug purposes:

```php
use Embed\Embed;

//Get the info
$info = Embed::create('https://www.youtube.com/watch?v=PP1xn5wHtxE');

//Get all providers
$providers = $info->getProviders();

//Get the oembed provider
$oembed = $providers['oembed'];

//Get the oembed title value:
echo $oembed->getTitle();

//Get any value returned by the oembed api
echo $oembed->bag->get('author_name');

//Get the main response object
$response = $info->getResponse();

//Get any http response header
$lastModified = $response->getHeader('Last-Modifier');

//Get the html body as DOMDocument
$html = $response->getHtmlContent();

//Get all http request executed (oembed endpoints, images, apis, etc...)
$dispatcher = $adapter->getDispatcher();

foreach ($dispatcher->getAllResponses() as $response) {
    echo 'The request to '.$response->getStartingUrl();
    echo ' is resolved to '.$response->getUrl();
}
```

---

If this library is useful for you, say thanks [buying me a beer :beer:](https://www.paypal.me/oscarotero)!

[ico-version]: https://poser.pugx.org/embed/embed/v/stable
[ico-travis]: https://travis-ci.org/oscarotero/Embed.svg?branch=master
[ico-license]: https://poser.pugx.org/embed/embed/license
[ico-scrutinizer]: https://scrutinizer-ci.com/g/oscarotero/Embed/badges/quality-score.png?s=79e37032db280b9795388124c030dcf4309343d1
[ico-sensiolabs]: https://insight.sensiolabs.com/projects/f0beab9f-fe41-47db-8806-373f80c50f9e/big.png
[ico-downloads]: https://poser.pugx.org/embed/embed/downloads
[ico-m-downloads]: https://poser.pugx.org/embed/embed/d/monthly
[ico-references]: https://www.versioneye.com/php/embed:embed/reference_badge.svg?style=flat

[link-packagist]: https://packagist.org/packages/embed/embed
[link-travis]: https://travis-ci.org/oscarotero/Embed
[link-scrutinizer]: https://scrutinizer-ci.com/g/oscarotero/Embed/
[link-sensiolabs]: https://insight.sensiolabs.com/projects/f0beab9f-fe41-47db-8806-373f80c50f9e
[link-references]: https://www.versioneye.com/php/embed:embed/references


embed/embed

type: library

description: PHP library to retrieve page info using oembed, opengraph, etc

keywords:
oembed
opengraph
twitter cards
embed
embedly

license: MIT

Oscar Otero

role: Developer

Embed\: src

Embed\Tests\: tests/

demo: php -S localhost:8888 demo/index.php

test: phpunit

cs-fix: php-cs-fixer fix .

repo_name: spookie/gnu

license: mit


code:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
    backupGlobals="false"
    backupStaticAttributes="false"
    colors="true"
    verbose="true"
    convertErrorsToExceptions="true"
    convertNoticesToExceptions="true"
    convertWarningsToExceptions="true"
    processIsolation="false"
    stopOnFailure="false">
	<testsuites>
		<testsuite name="All tests">
			<directory>tests</directory>
		</testsuite>
	</testsuites>
    <groups>
        <exclude>
            <group>ignore</group>
        </exclude>
    </groups>
</phpunit>


code:
<?php

namespace Embed\Adapters;

use Embed\Bag;
use Embed\DataInterface;
use Embed\Http\DispatcherInterface;
use Embed\Http\ImageResponse;
use Embed\Http\Response;
use Embed\Http\Url;
use Embed\Providers\Provider;
use Embed\Utils;
use Exception;

/**
 * Base class extended by all adapters.
 *
 * @property null|string  $title
 * @property null|string  $description
 * @property null|string  $url
 * @property null|string  $type
 * @property array        $tags
 * @property array        $feeds
 * @property array        $images
 * @property array        $imagesUrls
 * @property null|string  $image
 * @property null|int     $imageWidth
 * @property null|int     $imageHeight
 * @property null|string  $code
 * @property null|int     $width
 * @property null|int     $height
 * @property null|float   $aspectRatio
 * @property null|string  $authorName
 * @property null|string  $authorUrl
 * @property array        $providerIcons
 * @property array        $providerIconsUrls
 * @property null|string  $providerIcon
 * @property null|string  $providerName
 * @property null|string  $providerUrl
 * @property null|string  $publishedTime
 */
abstract class Adapter implements DataInterface
{
    protected $config;
    protected $dispatcher;
    protected $response;
    protected $providers = [];

    /**
     * Checks whether the response is valid to this Adapter.
     *
     * @param Response $response
     *
     * @return bool
     */
    public static function check(Response $response)
    {
        return $response->isValid();
    }

    /**
     * Constructor.
     *
     * @param Response  $response
     * @param array $config
     * @param DispatcherInterface $dispatcher
     */
    public function __construct(Response $response, array $config, DispatcherInterface $dispatcher)
    {
        $this->response = $response;
        $this->config = new Bag($config);
        $this->dispatcher = $dispatcher;

        $this->init();
    }

    /**
     * Initialize the providers
     */
    abstract protected function init();

    /**
     * Magic method to execute methods to return paramaters
     * For example, $source->sourceUrl executes $source->getSourceUrl().
     *
     * @param string $name The property name
     *
     * @return mixed
     */
    public function __get($name)
    {
        $method = 'get'.$name;

        if (method_exists($this, $method)) {
            return $this->$name = $this->$method();
        }
        return null;
    }

    /**
     * Magic method to execute methods to check if parameters are empty
     *
     * @param string $name The property name
     *
     * @return bool
     */
    public function __isset($name)
    {
        $value = $this->$name;

        return !empty($value);
    }

    /**
     * Returns the dispatcher used.
     *
     * @return DispatcherInterface
     */
    public function getDispatcher()
    {
        return $this->dispatcher;
    }

    /**
     * Returns the main response instance.
     *
     * @return Response
     */
    public function getResponse()
    {
        return $this->response;
    }

    /**
     * Get a config value.
     *
     * @param string $name
     * @param mixed  $default
     *
     * @return string|null
     */
    public function getConfig($name, $default = null)
    {
        $value = $this->config->get($name);

        return $value === null ? $default : $value;
    }

    /**
     * Get all providers.
     *
     * @return Provider[]
     */
    public function getProviders()
    {
        return $this->providers;
    }

    /**
     * {@inheritdoc}
     */
    public function getTitle()
    {
        $default = $this->url;

        return $this->getFirstFromProviders(function (Provider $provider) {
            return $provider->getTitle();
        }, $default);
    }

    /**
     * {@inheritdoc}
     */
    public function getDescription()
    {
        return $this->getFirstFromProviders(function (Provider $provider) {
            return $provider->getDescription();
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getType()
    {
        $code = $this->code;

        if (empty($code)) {
            return 'link';
        }

        if (strpos($code, '</video>')) {
            return 'video';
        }

        //Get the most repeated type
        $types = [];

        foreach ($this->providers as $provider) {
            $type = $provider->getType();

            if ($type !== null) {
                if (!isset($types[$type])) {
                    $types[$type] = 0;
                }

                ++$types[$type];
            }
        }

        //If it has code, it's not a link
        unset($types['link']);

        return static::getBigger($types) ?: 'rich';
    }

    /**
     * {@inheritdoc}
     */
    public function getTags()
    {
        return $this->getAllFromProviders(function (Provider $provider) {
            return $provider->getTags();
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getFeeds()
    {
        return $this->getAllFromProviders(function (Provider $provider) {
            return $provider->getFeeds();
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        //Width and height deppends to the code
        $this->width = null;
        $this->height = null;

        $codes = [];

        foreach ($this->providers as $provider) {
            $code = $provider->getCode();

            if (!empty($code)) {
                $codes[] = [
                    'code' => $code,
                    'width' => $provider->getWidth(),
                    'height' => $provider->getHeight(),
                ];
            }
        }

        if (empty($codes)) {
            return null;
        }

        //Use only html5 codes
        $html5 = array_filter($codes, function (array $code) {
            return strpos($code['code'], '</object>') === false && strpos($code['code'], '</embed>') === false;
        });

        //If it's not html5 codes, returns flash
        if (empty($html5)) {
            $code = current($codes);
        } else {
            $code = current($html5);
        }

        $this->width = is_numeric($code['width']) ? (int) $code['width'] : $code['width'];
        $this->height = is_numeric($code['height']) ? (int) $code['height'] : $code['height'];

        return $code['code'];
    }

    /**
     * Returns the code as DOMNodeList elements
     *
     * @return \DOMNodeList|null
     */
    public function getHtml()
    {
        $code = $this->code;

        if (empty($code)) {
            return;
        }

        $errors = libxml_use_internal_errors(true);
        $entities = libxml_disable_entity_loader(true);

        $dom = new \DOMDocument();
        $dom->loadHTML($code);

        libxml_use_internal_errors($errors);
        libxml_disable_entity_loader($entities);

        return Utils::xpathQuery($dom, 'descendant-or-self::body/*', false);
    }

    /**
     * {@inheritdoc}
     */
    public function getUrl()
    {
        $default = (string) $this->getResponse()->getUrl();
        $blacklist = $this->getConfig('url_blacklist');

        //some sites returns the homepage as canonical
        $homeUrl = $this->getResponse()->getUrl()->getAbsolute('/');

        return $this->getFirstFromProviders(function (Provider $provider) use ($blacklist, $homeUrl) {
            $url = $provider->getUrl();

            try {
                if ($homeUrl === $url || (!empty($blacklist) && Url::create($url)->match($blacklist))) {
                    return false;
                }
            } catch (Exception $error) {
                return false;
            }

            return $url;
        }, $default);
    }

    /**
     * {@inheritdoc}
     */
    public function getAuthorName()
    {
        return $this->getFirstFromProviders(function (Provider $provider) {
            return $provider->getAuthorName();
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getAuthorUrl()
    {
        return $this->getFirstFromProviders(function (Provider $provider) {
            return $provider->getAuthorUrl();
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getProviderIconsUrls()
    {
        $urls = [
            $this->getResponse()->getUrl()->getAbsolute('/favicon.ico'),
            $this->getResponse()->getUrl()->getAbsolute('/favicon.png'),
        ];

        foreach ($this->providers as $provider) {
            foreach ($provider->getProviderIconsUrls() as $url) {
                if (!in_array($url, $urls, true)) {
                    $urls[] = $url;
                }
            }
        }

        return $urls;
    }

    /**
     * Gets all icon provider urls found
     * It returns also the width, height and mime-type.
     *
     * @return array
     */
    public function getProviderIcons()
    {
        return $this->dispatchImagesInfo($this->providerIconsUrls);
    }

    /**
     * Gets the best icon provider
     *
     * @return string|null
     */
    public function getProviderIcon()
    {
        $icons = $this->providerIcons;

        if (empty($icons)) {
            return null;
        }

        $sizes = [];

        foreach ($icons as $icon) {
            $sizes[$icon['url']] = $icon['size'];
        }

        return static::getBigger($sizes);
    }

    /**
     * {@inheritdoc}
     */
    public function getProviderName()
    {
        $default = $this->getResponse()->getUrl()->getDomain();

        return $this->getFirstFromProviders(function (Provider $provider) {
            return $provider->getProviderName();
        }, $default);
    }

    /**
     * {@inheritdoc}
     */
    public function getProviderUrl()
    {
        $url = $this->getResponse()->getUrl();
        $default = $url->getScheme().'://'.$url->getDomain(true);

        return $this->getFirstFromProviders(function (Provider $provider) {
            return $provider->getProviderUrl();
        }, $default);
    }

    /**
     * {@inheritdoc}
     */
    public function getImagesUrls()
    {
        $urls = $this->getAllFromProviders(function (Provider $provider) {
            return $provider->getImagesUrls();
        });

        $blacklist = $this->getConfig('images_blacklist');

        if (!empty($blacklist)) {
            $urls = array_filter($urls, function ($url) use ($blacklist) {
                return !Url::create($url)->match($blacklist);
            });
        }

        return array_values($urls);
    }

    /**
     * Gets all images found in the webpage
     * It returns also the width, height and mime-type.
     *
     * @return array
     */
    public function getImages()
    {
        return $this->dispatchImagesInfo($this->imagesUrls);
    }

    /**
     * Gets the best image
     *
     * @return string|null
     */
    public function getImage()
    {
        $this->imageWidth = null;
        $this->imageHeight = null;

        $images = $this->images;
        $bigger = (bool) $this->getConfig('choose_bigger_image');
        $minWidth = $this->getConfig('min_image_width', 1);
        $minHeight = $this->getConfig('min_image_height', 1);

        $images = array_filter($images, function ($image) use ($minWidth, $minHeight) {
            return $image['width'] >= $minWidth && $image['height'] >= $minHeight;
        });

        if (empty($images)) {
            return null;
        }

        reset($images);
        $image = current($images);

        if ($bigger) {
            $sizes = [];

            foreach ($images as $img) {
                $sizes[$img['url']] = $img['size'];
            }

            $biggest = static::getBigger($sizes);

            foreach ($images as $img) {
                if ($biggest == $img['url']) {
                    $image = $img;
                    break;
                }
            }
        }

        $this->imageWidth = $image['width'];
        $this->imageHeight = $image['height'];

        return $image['url'];
    }

    /**
     * Gets the image width.
     *
     * @return int|null
     */
    public function getImageWidth()
    {
        $this->image = $this->getImage();

        return $this->imageWidth;
    }

    /**
     * Gets the image height.
     *
     * @return int|null
     */
    public function getImageHeight()
    {
        $this->image = $this->getImage();

        return $this->imageHeight;
    }

    /**
     * {@inheritdoc}
     */
    public function getWidth()
    {
        $this->code = $this->getCode();

        return $this->width;
    }

    /**
     * {@inheritdoc}
     */
    public function getHeight()
    {
        $this->code = $this->getCode();

        return $this->height;
    }

    /**
     * Gets the aspect ratio of the embedded widget
     * (useful to make it responsive).
     *
     * @return float|null
     */
    public function getAspectRatio()
    {
        if ($this->width !== null
         && (strpos($this->width, '%') === false)
         && $this->height !== null && (strpos($this->height, '%') === false)) {
            return round(($this->height / $this->width) * 100, 3);
        }
        return null;
    }

    /**
     * {@inheritdoc}
     */
    public function getPublishedTime()
    {
        return $this->getFirstFromProviders(function (Provider $provider) {
            return $provider->getPublishedTime();
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getLicense()
    {
        return $this->getFirstFromProviders(function (Provider $provider) {
            return $provider->getLicense();
        });
    }

    /**
     * {@inheritdoc}
     */
    public function getLinkedData()
    {
        return $this->getAllFromProviders(function (Provider $provider) {
            return $provider->getLinkedData();
        });
    }

    /**
     * Get images info.
     *
     * @param array $urls
     *
     * @return array
     */
    private function dispatchImagesInfo($urls)
    {
        if (empty($urls)) {
            return [];
        }

        $requests = [];

        foreach ($urls as $url) {
            $requests[] = Url::create($url);
        }

        return array_map(
            function (ImageResponse $response) {
                return [
                    'url' => (string) $response->getUrl(),
                    'width' => $response->getWidth(),
                    'height' => $response->getHeight(),
                    'size' => $response->getWidth() * $response->getHeight(),
                    'mime' => $response->getContentType(),
                ];
            },
            $this->getDispatcher()->dispatchImages($requests)
        );
    }

    /**
     * Returns the key of the bigger value in an array
     *
     * @param array $values
     *
     * @return string|null
     */
    protected static function getBigger(array $values)
    {
        $bigger = null;

        foreach ($values as $value => $repeat) {
            if ($bigger === null || $repeat > $values[$bigger]) {
                $bigger = $value;
            }
        }

        return $bigger;
    }

    /**
     * Returns the first value of the providers
     *
     * @param callable $callable
     * @param string|null $default
     *
     * @return string|null
     */
    protected function getFirstFromProviders(callable $callable, $default = null)
    {
        $values = array_filter(array_map($callable, $this->providers));

        return empty($values) ? $default : current($values);
    }

    /**
     * Returns the all values from the providers
     *
     * @param callable $callable
     *
     * @return array
     */
    protected function getAllFromProviders(callable $callable)
    {
        $values = array_filter(array_map($callable, $this->providers));
        $all = [];

        foreach ($values as $value) {
            foreach ($value as $v) {
                if (!in_array($v, $all, true)) {
                    $all[] = $v;
                }
            }
        }

        return $all;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;
use Embed\Providers\Api;

/**
 * Adapter to provide information from archive.org API.
 */
class Archive extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'archive.org/details/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        parent::init();

        $this->providers = ['archive' => new Api\Archive($this)] + $this->providers;
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        return Utils::iframe(str_replace('/details/', '/embed/', $this->url), $this->width, $this->height);
    }

    /**
     * {@inheritdoc}
     */
    public function getWidth()
    {
        return 640;
    }

    /**
     * {@inheritdoc}
     */
    public function getHeight()
    {
        return 480;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Utils;
use Embed\Http\Response;

/**
 * Adapter to get the embed code from play.cadenaser.com.
 */
class Cadenaser extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'play.cadenaser.com/audio/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $url = $this->getResponse()->getUrl();

        return Utils::iframe($url->withPath('/widget/'.$url->getPath()), $this->width, $this->height);
    }

    /**
     * {@inheritdoc}
     */
    public function getWidth()
    {
        return 620;
    }

    /**
     * {@inheritdoc}
     */
    public function getHeight()
    {
        return 100;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;

/**
 * Adapter to get the embed code from cartodb.
 */
class Carto extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            '*.carto.com/viz/*/public_map',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = 520;

        $url = $this->getResponse()->getUrl()->withDirectoryPosition(2, 'embed_map');

        return Utils::iframe($url, '100%', $this->height);
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Http\Url;
use Embed\Utils;

/**
 * Adapter provider more information from cnn.com.
 */
class Cnn extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            '*.cnn.com/videos/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $url = Url::create('https://fave.api.cnn.io/v1/fav/')
            ->withQueryParameters([
                'video' => implode('/', $this->getResponse()->getUrl()->getSlicePath(1)),
                'customer' => 'cnn',
                'env' => 'prod'
            ]);

        $this->width = 416;
        $this->height = 234;

        return Utils::iframe($url, $this->width, $this->height);
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;
use Embed\Providers\Api;

/**
 * Adapter provider more information from some facebook pages.
 */
class Facebook extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'www.facebook.com/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        parent::init();

        $this->providers = ['facebook' => new Api\Facebook($this)] + $this->providers;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;
use Embed\Providers;

/**
 * Adapter to provide information from raw files.
 */
class File extends Adapter
{
    private static $contentTypes = [
        'video/ogg' => ['video', 'videoHtml'],
        'video/quicktime' => ['video', 'videoHtml'],
        'application/ogg' => ['video', 'videoHtml'],
        'video/ogv' => ['video', 'videoHtml'],
        'video/webm' => ['video', 'videoHtml'],
        'video/mp4' => ['video', 'videoHtml'],
        'audio/ogg' => ['audio', 'audioHtml'],
        'audio/mp3' => ['audio', 'audioHtml'],
        'audio/mpeg' => ['audio', 'audioHtml'],
        'audio/webm' => ['audio', 'audioHtml'],
        'image/jpeg' => ['photo', 'imageHtml'],
        'image/jpg' => ['photo', 'imageHtml'],
        'image/gif' => ['photo', 'imageHtml'],
        'image/png' => ['photo', 'imageHtml'],
        'image/bmp' => ['photo', 'imageHtml'],
        'image/ico' => ['photo', 'imageHtml'],
        'image/webp' => ['photo', 'imageHtml'],
        'text/rtf' => ['rich', 'google'],
        'application/pdf' => ['rich', 'google'],
        'application/msword' => ['rich', 'google'],
        'application/vnd.ms-powerpoint' => ['rich', 'google'],
        'application/vnd.ms-excel' => ['rich', 'google'],
        'application/zip' => ['rich', 'google'],
        'application/postscript' => ['rich', 'google'],
        'application/octet-stream' => ['rich', 'google'],
    ];

    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && isset(self::$contentTypes[$response->getContentType()]);
    }

    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        $this->providers = [
            'oembed' => new Providers\OEmbed($this),
        ];
    }

    /**
     * {@inheritdoc}
     */
    public function getType()
    {
        return self::$contentTypes[$this->getResponse()->getContentType()][0];
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        if (($code = parent::getcode())) {
            return $code;
        }

        switch (self::$contentTypes[$this->getResponse()->getContentType()][1]) {
            case 'videoHtml':
                return Utils::videoHtml($this->image, $this->url, $this->imageWidth, $this->imageHeight);

            case 'audioHtml':
                return Utils::audioHtml($this->url);

            case 'google':
                return Utils::google($this->url);

            case 'imageHtml':
                return Utils::imageHtml($this->url, 'Remote file', $this->imageWidth, $this->imageHeight);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getImagesUrls()
    {
        if ($this->type === 'photo') {
            return [$this->url];
        }

        return [];
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;

/**
 * Adapter provider more information from flickr.
 */
class Flickr extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'www.flickr.com/photos/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $code = parent::getCode();

        if (empty($code)) {
            $this->width = 640;
            $this->height = 425;

            $code = Utils::iframe($this->getResponse()->getUrl()->withAddedPath('player'), $this->width, $this->height);
        }

        return $code;
    }

    /**
     * {@inheritdoc}
     */
    public function getProviderName()
    {
        return 'Flickr';
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Providers\Api;

/**
 * Adapter to get the embed code from gist.github.com.
 */
class Github extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'gist.github.com/*/*',
            'github.com/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        parent::init();

        if ($this->getResponse()->getUrl()->getHost() === 'gist.github.com') {
            $this->providers = ['gist' => new Api\Gist($this)] + $this->providers;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getUrl()
    {
        //Open graph returns as canonical url the main repo url, instead the file
        return (string) $this->getResponse()->getUrl();
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = null;

        $url = $this->getResponse()->getUrl();

        if ($url->match('github.com/*/*/blob/*')) {
            $username = $url->getDirectoryPosition(0);
            $repo = $url->getDirectoryPosition(1);
            $ref = $url->getDirectoryPosition(3);
            $path_to_file = implode('/', $url->getSlicePath(4));

            switch ($url->getExtension()) {
                case 'geojson':
                    //https://help.github.com/articles/mapping-geojson-files-on-github/#embedding-your-map-elsewhere
                    return "<script src=\"https://embed.githubusercontent.com/view/geojson/{$username}/{$repo}/{$ref}/{$path_to_file}\"></script>";

                case 'stl':
                    //https://help.github.com/articles/3d-file-viewer/#embedding-your-model-elsewhere
                    return "<script src=\"https://embed.githubusercontent.com/view/3d/{$username}/{$repo}/{$ref}/{$path_to_file}\"></script>";
            }
        }

        return parent::getCode();
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;
use Embed\Providers\Api;

/**
 * Adapter provider more information from google maps and google drive.
 */
class Google extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'maps.google.*',
            'www.google.*/maps*',
            'calendar.google.com/calendar/*',
            'drive.google.com/file/*/view',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        parent::init();

        if ($this->getResponse()->getUrl()->match('*/maps/*')) {
            $this->providers = ['google' => new Api\GoogleMaps($this)] + $this->providers;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = null;

        $url = $this->getResponse()->getUrl();

        if ($url->getHost() === 'calendar.google.com') {
            return Utils::iframe($url);
        }

        if (isset($this->providers['google'])) {
            return $this->providers['google']->getCode();
        }

        return Utils::iframe($url
            ->withDirectoryPosition(3, 'preview')
            ->withQueryParameters([]));
    }

    /**
     * {@inheritdoc}
     */
    public function getImagesUrls()
    {
        return [];
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;

/**
 * Adapter to get the embed code from howcast.com.
 */
class Howcast extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'www.howcast.com/videos/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = null;

        if (!($dom = $this->getResponse()->getHtmlContent())) {
            return;
        }

        // #embedModal textarea
        $textarea = Utils::xpathQuery($dom, "descendant-or-self::*[@id = 'embedModal']/descendant-or-self::*/textarea");

        if ($textarea) {
            return $textarea->nodeValue;
        }
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;

/**
 * Adapter to generate embed code from ideone.com.
 */
class Ideone extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'ideone.com/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = null;

        $url = $this->getResponse()->getUrl();
        $path = '/e.js'.$url->getPath();

        return Utils::script($url->getAbsolute($path));
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Providers\Api;

/**
 * Adapter to provide information from imageshack.
 */
class Imageshack extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'imageshack.com/i/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        parent::init();

        $this->providers = ['imageshack' => new Api\Imageshack($this)] + $this->providers;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Utils;

/**
 * Adapter to fix some issues from jsfiddle.
 */
class Jsfiddle extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = null;

        $url = $this->url;
        $embed_url = $url.((substr($url, -1) === '/') ? 'embedded/' : '/embedded/');

        return Utils::iframe($embed_url);
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;

/**
 * Adapter to provide all information from lavozdegalicia.es that needs a special query parameter to generate a session cookie.
 */
class Lavozdegalicia extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'www.lavozdegalicia.es/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        parent::init();

        $url = $this->getResponse()->getStartingUrl();

        $this->response = $this->getDispatcher()->dispatch($url->withQueryParameter('piano_d', '1'));
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Utils;
use Embed\Http\Response;

/**
 * Adapter to get the embed code from line.do.
 */
class Line extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'line.do/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $url = $this->getResponse()->getUrl();
        $id = $url->getDirectoryPosition(2);

        return Utils::iframe($url->withPath("embed/{$id}/vertical"), $this->width, $this->height, 'border:1px solid #e7e7e7;');
    }

    /**
     * {@inheritdoc}
     */
    public function getWidth()
    {
        return 640;
    }

    /**
     * {@inheritdoc}
     */
    public function getHeight()
    {
        return 640;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;

/**
 * Adapter get embed code from 500px.com.
 */
class N500px extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            '500px.com/photo/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $url = $this->getResponse()->getUrl();

        if (is_numeric($url->getDirectoryPosition(1))) {
            return Utils::iframe($url->withDirectoryPosition(2, 'embed.html'), $this->width, $this->height);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getWidth()
    {
        if (is_numeric($this->getResponse()->getUrl()->getDirectoryPosition(1))) {
            return $this->imageWidth;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function getHeight()
    {
        if (is_numeric($this->getResponse()->getUrl()->getDirectoryPosition(1))) {
            return $this->imageHeight;
        }
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;

/**
 * Adapter to get more info from parleys.com.
 */
class Parleys extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'www.parleys.com/play/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $id = $this->getResponse()->getUrl()->getDirectoryPosition(1);

        return '
width.';height:'.$this->height.'px">
';
    }

    /**
     * {@inheritdoc}
     */
    public function getWidth()
    {
        return '100%';
    }

    /**
     * {@inheritdoc}
     */
    public function getHeight()
    {
        return 300;
    }

    /**
     * {@inheritdoc}
     */
    public function getType()
    {
        return 'video';
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;

/**
 * Adapter to generate embed code from pastebin.
 */
class Pastebin extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'pastebin.com/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = null;

        $url = $this->getResponse()->getUrl();
        $path = '/embed_js' . $url->getPath();

        return Utils::script($this->getResponse()->getUrl()->getAbsolute($path));
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Utils;

/**
 * Adapter to generate embed code from pastie.org.
 */
class Pastie extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'pastie.org/pastes/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = null;

        $path = '/'.$this->getResponse()->getUrl()->getDirectoryPosition(1).'.js';

        return Utils::script($this->getResponse()->getUrl()->getAbsolute($path));
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;

/**
 * Adapter to get embed code from pinterest.com.
 */
class Pinterest extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'www.pinterest.com/pin/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        return 'getResponse()->getUrl()
            .'">'
            .'';
    }

    /**
     * {@inheritdoc}
     */
    public function getWidth()
    {
        return 236;
    }

    /**
     * {@inheritdoc}
     */
    public function getHeight()
    {
        return 442;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;

/**
 * Adapter to generate embed code from SassMeister.
 */
class Sassmeister extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'sassmeister.com/gist/*',
            'www.sassmeister.com/gist/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = 480;
        $id = $this->getResponse()->getUrl()->getDirectoryPosition(1);

        return "
".
               "Play with this gist on SassMeister.".
               '
'.
               '';
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Utils;
use Embed\Http\Response;

/**
 * Adapter to get the embed code from slides.com.
 */
class Slides extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'slides.com/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        return Utils::iframe($this->getResponse()->getUrl()->withAddedPath('embed'), $this->width, $this->height);
    }

    /**
     * {@inheritdoc}
     */
    public function getWidth()
    {
        return 576;
    }

    /**
     * {@inheritdoc}
     */
    public function getHeight()
    {
        return 420;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;

/**
 * Adapter to generate embed code from snipplr.com.
 */
class Snipplr extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            'snipplr.com/view/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCode()
    {
        $this->width = null;
        $this->height = null;

        $id = $this->getResponse()->getUrl()->getDirectoryPosition(1);

        return <<<CODE

View this snippet on Snipplr

CODE;
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;

/**
 * Adapter to provide information from Vimeo.
 * Required when Vimeo returns a 403 status code.
 */
class Vimeo extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid([200, 403]) && $response->getUrl()->match([
            'vimeo.com/*',
        ]);
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Providers;

/**
 * Adapter to provide all information from any webpage.
 */
class Webpage extends Adapter
{
    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        $this->providers = [
            'oembed' => new Providers\OEmbed($this),
            'opengraph' => new Providers\OpenGraph($this),
            'linkpulse' => new Providers\Linkpulse($this),
            'twittercards' => new Providers\TwitterCards($this),
            'dcterms' => new Providers\Dcterms($this),
            'sailthru' => new Providers\Sailthru($this),
            'html' => new Providers\Html($this),
        ];
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;
use Embed\Providers\Api;

/**
 * Adapter to provide information from wikipedia.
 */
class Wikipedia extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid() && $response->getUrl()->match([
            '*.wikipedia.org/wiki/*',
        ]);
    }

    /**
     * {@inheritdoc}
     */
    protected function init()
    {
        parent::init();

        $this->providers = ['wikipedia' => new Api\Wikipedia($this)] + $this->providers;
    }

    /**
     * {@inheritdoc}
     */
    public function getProviderName()
    {
        return 'Wikipedia';
    }
}


code:
<?php

namespace Embed\Adapters;

use Embed\Http\Response;

/**
 * Adapter to provide information from youtube.
 * Required when youtube returns a 429 status code.
 */
class Youtube extends Webpage
{
    /**
     * {@inheritdoc}
     */
    public static function check(Response $response)
    {
        return $response->isValid([200, 429]) && $response->getUrl()->match([
            '*.youtube.*',
        ]);
    }
}


code:
<?php

namespace Embed;

/**
 * Class to store and access to data.
 */
class Bag
{
    private $parameters = [];

    /**
     * Set the initial parameters.
     *
     * @param array $parameters
     */
    public function __construct(array $parameters = [])
    {
        $this->set($parameters);
    }

    /**
     * Save a value.
     *
     * @param string|array $name
     * @param mixed        $value
     */
    public function set($name, $value = null)
    {
        if (!is_array($name)) {
            $name = [$name => $value];
        }

        foreach ($name as $name => $value) {
            $name = self::normalizeName($name);
            $value = self::normalizeValue($value);

            $this->parameters[$name] = $value;
        }
    }

    /**
     * Adds a subvalue.
     *
     * @param string $name
     * @param mixed  $value
     */
    public function add($name, $value = null)
    {
        $name = self::normalizeName($name);
        $value = self::normalizeValue($value);

        if (!isset($this->parameters[$name])) {
            $this->parameters[$name] = [];
        } elseif (!is_array($this->parameters[$name])) {
            $this->parameters[$name] = (array) $this->parameters[$name];
        }

        $this->parameters[$name][] = $value;
    }

    /**
     * Get a value.
     *
     * @param string $name
     * @param bool   $isHtml
     *
     * @return string|null
     */
    public function get($name, $isHtml = false)
    {
        $name = self::normalizeName($name);

        if (strpos($name, '[') !== false) {
            $names = explode('[', str_replace(']', '', $name));
            $key = array_shift($names);
            $item = isset($this->parameters[$key]) ? $this->parameters[$key] : [];

            foreach ($names as $key) {
                if (!isset($item[$key])) {
                    return;
                }

                $item = $item[$key];
            }

            return $isHtml ? $item : self::toPlainValue($item);
        }

        if (isset($this->parameters[$name])) {
            return $isHtml ? $this->parameters[$name] : self::toPlainValue($this->parameters[$name]);
        }
    }

    /**
     * Return all stored values keys.
     *
     * @return array
     */
    public function getKeys()
    {
        return array_keys($this->parameters);
    }

    /**
     * Return the raw stored values.
     *
     * @return array
     */
    public function getAll()
    {
        return $this->parameters;
    }

    /**
     * Check if a value exists and is not empty.
     *
     * @param string $name
     *
     * @return bool
     */
    public function has($name)
    {
        $name = self::normalizeName($name);

        if (strpos($name, '[') !== false) {
            $names = explode('[', str_replace(']', '', $name));
            $key = array_shift($names);
            $item = isset($this->parameters[$key]) ? $this->parameters[$key] : [];

            foreach ($names as $key) {
                if (!isset($item[$key])) {
                    return false;
                }

                $item = $item[$key];
            }

            return !empty($item);
        }

        return !empty($this->parameters[$name]);
    }

    /**
     * Normalize a variable name.
     *
     * @param string $name
     *
     * @return string
     */
    private static function normalizeName($name)
    {
        return strtolower(trim($name));
    }

    /**
     * Normalize a value.
     * If it's a string, removes spaces and normalize some utf-8 chars.
     *
     * @param mixed $value
     *
     * @return mixed
     */
    private static function normalizeValue($value)
    {
        if (is_string($value)) {
            $value = str_ireplace([' ', ' '], ' ', $value);
            $value = trim($value);

            return ($value === '') ? null : $value;
        }

        return $value;
    }

    /**
     * Remove the html code and entities in a value
     *
     * @param mixed $value
     *
     * @return mixed
     */
    private static function toPlainValue($value)
    {
        if (is_string($value)) {
            $value = strip_tags($value);
            $value = html_entity_decode($value, ENT_QUOTES | ENT_XML1, 'UTF-8');
            $value = trim($value);

            return ($value === '') ? null : $value;
        }

        return $value;
    }
}


code:
<?php

namespace Embed;

/**
 * Interface used by all adapters and providers.
 */
interface DataInterface
{
    /**
     * Gets the title.
     *
     * @return string|null
     */
    public function getTitle();

    /**
     * Gets the description.
     *
     * @return string|null
     */
    public function getDescription();

    /**
     * Gets the type of the url
     * The types are the same than the oEmbed types:
     * video, photo, link, rich.
     *
     * @return string|null
     */
    public function getType();

    /**
     * Gets the tags of the url.
     *
     * @return array
     */
    public function getTags();

    /**
     * Gets the feeds urls.
     *
     * @return array
     */
    public function getFeeds();

    /**
     * Gets the embed code.
     *
     * @return string|null
     */
    public function getCode();

    /**
     * Gets the canonical url.
     *
     * @return string|null
     */
    public function getUrl();

    /**
     * Gets the author name.
     *
     * @return string|null
     */
    public function getAuthorName();

    /**
     * Gets the author url.
     *
     * @return string|null
     */
    public function getAuthorUrl();

    /**
     * Gets the urls of all icons of the provider
     * Note: it doesn't check whether the image exists or not.
     *
     * @return array
     */
    public function getProviderIconsUrls();

    /**
     * Gets the provider name.
     *
     * @return string|null
     */
    public function getProviderName();

    /**
     * Gets the provider url (usually the home url of the link).
     *
     * @return string|null
     */
    public function getProviderUrl();

    /**
     * Gets the urls of all images found in the webpage
     * Note: it doesn't check whether the image exists or not.
     *
     * @return array
     */
    public function getImagesUrls();

    /**
     * Gets the width of the embedded widget.
     *
     * @return int|null
     */
    public function getWidth();

    /**
     * Gets the height of the embedded widget.
     *
     * @return int|null
     */
    public function getHeight();

    /**
     * Gets the published time, if the webpage is an article.
     *
     * @return string|null
     */
    public function getPublishedTime();

    /**
     * Gets the license info.
     *
     * @return string|null
     */
    public function getLicense();

    /**
     * Returns all linked data found.
     *
     * @return array
     */
    public function getLinkedData();
}


code:
<?php

namespace Embed;

use Embed\Adapters\Adapter;
use Embed\Http\CurlDispatcher;
use Embed\Http\DispatcherInterface;
use Embed\Http\Url;

abstract class Embed
{
    /**
     * @var array
     */
    public static $default_config = [
        'custom_adapters_namespace' => null,
        'min_image_width' => 1,
        'min_image_height' => 1,
        'choose_bigger_image' => false,
        'images_blacklist' => [],
        'url_blacklist' => [
            '?&ns_campaign=*',
            '?&ns_source=*',
            '?&utm_campaign=*',
            '?&utm_medium=*',
            '?&utm_source=*',
        ],
        'follow_canonical' => true,

        'html' => [
            'max_images' => -1,
            'external_images' => false
        ],

        'oembed' => [
            'parameters' => [],
            'embedly_key' => null,
            'iframely_key' => null,
        ],

        'google' => [
            'key' => null,
        ],

        'soundcloud' => [
            'key' => null,
        ],

        'facebook' => [
            'key' => null,
            'events_fields' => 'id,cover,description,end_time,name,owner,place,start_time,timezone',
            'videos_fields' => 'id,description,embed_html',
        ]
    ];

    /**
     * Gets the info from an url.
     *
     * @param Url|string $url
     * @param array|null $config
     * @param DispatcherInterface|null $dispatcher
     *
     * @return Adapter
     */
    public static function create($url, array $config = null, DispatcherInterface $dispatcher = null)
    {
        if (!($url instanceof Url)) {
            $url = Url::create($url);
        }

        if ($config === null) {
            $config = self::$default_config;
        } else {
            $config += self::$default_config;
        }

        if ($dispatcher === null) {
            $dispatcher = new CurlDispatcher();
        }

        $info = self::process($url, $config, $dispatcher);

        if ($info->getConfig('follow_canonical') === false) {
            return $info;
        }

        // Repeat the process if:
        // - The canonical url is different
        // - No embed code has found
        $from = preg_replace('|^(\w+://)|', '', rtrim((string)$info->getResponse()->getUrl(), '/'));
        $to = preg_replace('|^(\w+://)|', '', rtrim($info->url, '/'));

        if ($from !== $to && empty($info->code)) {
            //accept new result if valid
            try {
                return self::process(Url::create($info->url), $config, $dispatcher);
            } catch (\Exception $e) {
                return $info;
            }
        }

        return $info;
    }

    /**
     * Process the url.
     *
     * @param Url $url
     * @param array   $config
     * @param DispatcherInterface   $dispatcher
     *
     * @throws Exceptions\InvalidUrlException If the urls is not valid
     *
     * @return Adapter
     */
    private static function process(Url $url, array $config, DispatcherInterface $dispatcher)
    {
        $response = $dispatcher->dispatch($url);

        //If is a file use File Adapter
        $adapter = self::getClass('File', $config);

        if ($adapter::check($response)) {
            return new $adapter($response, $config, $dispatcher);
        }

        //Search the adapter using the domain
        $adapter = self::getClass($response->getUrl()->getClassNameForDomain(), $config);

        if (class_exists($adapter) && $adapter::check($response)) {
            return new $adapter($response, $config, $dispatcher);
        }

        //Use the default webpage adapter
        $adapter = self::getClass('Webpage', $config);

        if ($adapter::check($response)) {
            return new $adapter($response, $config, $dispatcher);
        }

        if ($response->getError() === null) {
            $exception = new Exceptions\InvalidUrlException(sprintf("Invalid url '%s' (Status code %s)", (string) $url, $response->getStatusCode()));
        } else {
            $exception = new Exceptions\InvalidUrlException($response->getError());
        }

        $exception->setResponse($response);

        throw $exception;
    }

    /**
     * Returns a class name using the custom_adapters_namespace
     *
     * @param string $name
     * @param array $config
     *
     * @return  string
     */
    private static function getClass($name, array $config)
    {
        if (!empty($config['custom_adapters_namespace'])) {
            $namespaces = (array) $config['custom_adapters_namespace'];

            foreach ($namespaces as $namespace) {
                $class = $namespace.$name;

                if (class_exists($class)) {
                    return $class;
                }
            }
        }

        return 'Embed\\Adapters\\'.$name;
    }
}


code:
<?php

namespace Embed\Exceptions;

use Exception;

class EmbedException extends Exception
{
}


code:
<?php

namespace Embed\Exceptions;

use Embed\Http\Response;

class InvalidUrlException extends EmbedException
{
    /**
     * @var Response|null
     */
    private $response;

    /**
     * Set the response related with this error
     *
     * @param Response
     */
    public function setResponse(Response $response)
    {
        $this->response = $response;
    }

    /**
     * Get the response related with this error
     *
     * @return Response|null
     */
    public function getResponse()
    {
        return $this->response;
    }
}


code:
<?php

namespace Embed\Http;

/**
 * Class to consume http responses.
 */
abstract class AbstractResponse
{
    protected $startingUrl;
    protected $url;
    protected $statusCode;
    protected $contentType;
    protected $headers;
    protected $info;

    public function __construct(Url $startingUrl, Url $url, $statusCode, $contentType, array $headers, array $info)
    {
        $this->startingUrl = $startingUrl;
        $this->url = $url;
        $this->statusCode = (int)$statusCode;
        $this->contentType = $contentType;
        $this->headers = $headers;
        $this->info = $info;
    }

    /**
     * Get the starting url.
     *
     * @return Url
     */
    public function getStartingUrl()
    {
        return $this->startingUrl;
    }

    /**
     * Get the http code of the response, for example: 200.
     *
     * @return int
     */
    public function getStatusCode()
    {
        return $this->statusCode;
    }

    /**
     * Get the content-type of the response, for example: text/html.
     *
     * @return string|null
     */
    public function getContentType()
    {
        return $this->contentType;
    }

    /**
     * Returns the final url.
     *
     * @return Url
     */
    public function getUrl()
    {
        return $this->url;
    }

    /**
     * Returns the http headers.
     *
     * @return array
     */
    public function getHeaders()
    {
        return $this->headers;
    }

    /**
     * Returns extra http info.
     *
     * @return array
     */
    public function getInfo()
    {
        return $this->info;
    }

    /**
     * Returns dispatch error.
     *
     * @return string|null
     */
    public function getError()
    {
        return empty($this->info['error']) ? null : $this->info['error'];
    }

    /**
     * Get a header.
     *
     * @param string $name
     *
     * @return string|null
     */
    public function getHeader($name)
    {
        $name = strtolower($name);

        return isset($this->headers[$name]) ? implode(',', $this->headers[$name]) : null;
    }

    /**
     * Check if the response is valid or not.
     *
     * @param array $codes
     *
     * @return bool
     */
    public function isValid(array $codes = null)
    {
        if ($codes === null) {
            return $this->statusCode === 200;
        }

        return in_array($this->statusCode, $codes);
    }
}


code:
<?php

namespace Embed\Http;

use Embed\Exceptions\EmbedException;
use Composer\CaBundle\CaBundle;
use stdClass;

/**
 * Curl dispatcher.
 */
class CurlDispatcher implements DispatcherInterface
{
    private $responses = [];
    private static $acceptHeaders = [
        'jpg' => 'image/jpeg',
        'jpeg' => 'image/jpeg',
        'gif' => 'image/gif',
        'png' => 'image/png',
        'mp3' => 'audio/mpeg',
        'mp4' => 'video/mp4',
        'ogg' => 'audio/ogg',
        'ogv' => 'video/ogg',
        'webm' => 'video/webm',
    ];

    private $config = [
        CURLOPT_HTTPHEADER => [],
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT => 10,
        CURLOPT_SSL_VERIFYHOST => 0,
        CURLOPT_SSL_VERIFYPEER => false,
        CURLOPT_ENCODING => '',
        CURLOPT_AUTOREFERER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_USERAGENT => 'Embed PHP library',
        CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4,
    ];

    /**
     * Constructor.
     *
     * @param array $config
     */
    public function __construct(array $config = [])
    {
        $this->config = $config + $this->config;

        if (!isset($this->config[CURLOPT_COOKIEJAR])) {
            $cookies = str_replace('//', '/', sys_get_temp_dir().'/embed-cookies-'.uniqid().'.txt');

            if (is_file($cookies)) {
                if (!is_writable($cookies)) {
                    throw new EmbedException(sprintf('The temporary cookies file "%s" is not writable', $cookies));
                }
            } elseif (!is_writable(dirname($cookies))) {
                throw new EmbedException(sprintf('The temporary folder "%s" is not writable', dirname($cookies)));
            }

            $this->config[CURLOPT_COOKIEJAR] = $cookies;
            $this->config[CURLOPT_COOKIEFILE] = $cookies;
        }
    }

    /**
     * Return all responses for debug purposes
     *
     * @return AbstractResponse[]
     */
    public function getAllResponses()
    {
        return $this->responses;
    }

    /**
     * Remove the cookies file on destruct the instance.
     */
    public function __destruct()
    {
        $cookies = $this->config[CURLOPT_COOKIEJAR];

        if (is_file($cookies)) {
            unlink($cookies);
        }
    }

    /**
     * {@inheritdoc}
     */
    public function dispatch(Url $url)
    {
        $options = $this->config;

        $extension = $url->getExtension();

        if (!empty($extension) && isset(self::$acceptHeaders[$extension])) {
            $options[CURLOPT_HTTPHEADER][] = 'Accept: '.self::$acceptHeaders[$extension];
        } else {
            $options[CURLOPT_HTTPHEADER][] = 'Accept: */*';
        }

        $response = $this->exec($url, $options);

        //Some sites returns 403 with the default user-agent
        if ($response->getStatusCode() === 403) {
            $options[CURLOPT_USERAGENT] = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.95 Safari/537.36';

            return $this->exec($url, $options);
        }

        //Other sites needs a certificate
        if (
            class_exists('Composer\\CaBundle\\CaBundle')
         && $response->getStatusCode() === 0
         && strpos($response->getError(), 'SSL') !== false
        ) {
            $options[CURLOPT_SSL_VERIFYHOST] = 2;
            $options[CURLOPT_SSL_VERIFYPEER] = true;
            $options[CURLOPT_CAINFO] = CaBundle::getSystemCaRootBundlePath();

            return $this->exec($url, $options);
        }

        return $response;
    }

    /**
     * Execute a curl request
     *
     * @param Url   $url
     * @param array $options
     *
     * @return Response
     */
    protected function exec(Url $url, array $options)
    {
        $connection = curl_init((string) $url);
        curl_setopt_array($connection, $options);

        $curl = new CurlResult($connection);

        //Get only text responses
        $curl->onHeader(function ($name, $value, $data) {
            if ($name === 'content-type') {
                $data->isBinary = !preg_match('/(text|html|json)/', strtolower($value));
            }
        });

        $curl->onBody(function ($string, stdClass $data) {
            return empty($data->isBinary);
        });

        curl_exec($connection);

        $result = $curl->getResult();

        curl_close($connection);

        return $this->responses[] = new Response(
            $url,
            Url::create($result['url']),
            $result['statusCode'],
            $result['contentType'],
            $result['content'],
            $result['headers'],
            $result['info']
        );
    }

    /**
     * {@inheritdoc}
     */
    public function dispatchImages(array $urls)
    {
        if (empty($urls)) {
            return [];
        }

        $curl_multi = curl_multi_init();
        $responses = [];
        $connections = [];

        foreach ($urls as $k => $url) {
            if ($url->getScheme() === 'data') {
                $response = ImageResponse::createFromBase64($url);

                if ($response) {
                    $responses[$k] = $response;
                }

                continue;
            }

            $connection = curl_init((string) $url);

            $options = $this->config;
            $options[CURLOPT_HTTPHEADER] = ['Accept: image/*'];

            curl_setopt_array($connection, $options);
            curl_multi_add_handle($curl_multi, $connection);

            $curl = new CurlResult($connection);

            $curl->onBody(function ($body, stdClass $data) {
                if (($info = @getimagesizefromstring($body))) {
                    $data->width = $info[0];
                    $data->height = $info[1];
                    $data->mime = $info['mime'];

                    return false;
                }
            });

            $connections[$k] = $curl;
        }

        if (!empty($connections)) {
            do {
                $return = curl_multi_exec($curl_multi, $active);
            } while ($return === CURLM_CALL_MULTI_PERFORM);

            while ($active && $return === CURLM_OK) {
                if (curl_multi_select($curl_multi) === -1) {
                    usleep(100);
                }

                do {
                    $return = curl_multi_exec($curl_multi, $active);
                } while ($return === CURLM_CALL_MULTI_PERFORM);
            }

            foreach ($connections as $k => $connection) {
                $resource = $connection->getResource();

                curl_multi_remove_handle($curl_multi, $resource);
                $result = $connection->getResult();

                if (!empty($result['data']->mime)) {
                    $responses[$k] = $this->responses[] = new ImageResponse(
                        $urls[$k],
                        Url::create($result['url']),
                        $result['statusCode'],
                        $result['contentType'],
                        [$result['data']->width, $result['data']->height],
                        $result['headers'],
                        $result['info']
                    );
                }
            }
        }

        curl_multi_close($curl_multi);

        ksort($responses, SORT_NUMERIC);

        return array_values($responses);
    }
}


code:
<?php

namespace Embed\Http;

/**
 * Class to dispatch urls using curl and get the result.
 */
class CurlResult
{
    protected $resource;
    protected $body;
    protected $headers = [];
    protected $onHeader;
    protected $onBody;
    protected $data;

    /**
     * Creates a new response.
     *
     * @param resource $resource The curl resource
     */
    public function __construct($resource)
    {
        $this->resource = $resource;
        $this->data = (object) [];

        curl_setopt($this->resource, CURLOPT_HEADERFUNCTION, [$this, 'headerCallback']);
        curl_setopt($this->resource, CURLOPT_WRITEFUNCTION, [$this, 'writeCallback']);
    }

    /**
     * Returns the response result.
     */
    public function getResult()
    {
        $result = curl_getinfo($this->resource);

        if (curl_errno($this->resource)) {
            $result['error'] = curl_error($this->resource);
        }

        return [
            'url' => isset($result['url']) ? $result['url'] : null,
            'statusCode' => isset($result['http_code']) ? $result['http_code'] : null,
            'contentType' => isset($result['content_type']) ? $result['content_type'] : null,
            'info' => $result,
            'content' => $this->body,
            'headers' => $this->headers,
            'data' => $this->data,
        ];
    }

    /**
     * Returns the resource.
     *
     * @return resource
     */
    public function getResource()
    {
        return $this->resource;
    }

    /**
     * Callback used on receive a header.
     *
     * @param callable $callback
     */
    public function onHeader(callable $callback)
    {
        $this->onHeader = $callback;
    }

    /**
     * Callback used on receive a body string portion.
     *
     * @param callable $callback
     */
    public function onBody(callable $callback)
    {
        $this->onBody = $callback;
    }

    /**
     * Callback used to collect the headers.
     *
     * @param resource $resource
     * @param string   $string
     *
     * @return int
     */
    public function headerCallback($resource, $string)
    {
        if (!strpos($string, ':')) {
            return strlen($string);
        }

        list($name, $value) = array_map('trim', explode(':', $string, 2));

        $name = strtolower($name);

        if (!isset($this->headers[$name])) {
            $this->headers[$name] = [];
        }

        $this->headers[$name][] = $value;

        if ($this->onHeader === null || call_user_func($this->onHeader, $name, $value, $this->data) !== false) {
            return strlen($string);
        }

        return -1;
    }

    /**
     * Callback used to get the body content.
     *
     * @param resource $resource
     * @param string   $string
     *
     * @return int
     */
    public function writeCallback($resource, $string)
    {
        $this->body .= $string;

        if ($this->onBody === null || call_user_func($this->onBody, $this->body, $this->data) !== false) {
            return strlen($string);
        }

        //Cancel
        $this->body = null;

        return -1;
    }
}


code:
<?php

namespace Embed\Http;

/**
 * Interface used to create http responses.
 */
interface DispatcherInterface
{
    /**
     * Dispatch an url.
     *
     * @param Url $url
     *
     * @return Response
     */
    public function dispatch(Url $url);

    /**
     * Resolve multiple image urls at once.
     *
     * @param Url[] $urls
     *
     * @return ImageResponse[]
     */
    public function dispatchImages(array $urls);
}


code:
<?php

namespace Embed\Http;

/**
 * Class to consume http responses.
 */
class ImageResponse extends AbstractResponse
{
    protected $size;

    /**
     * Create a ImageResponse using a bas64 url.
     *
     * @param Url $url
     *
     * @return static|null
     */
    public static function createFromBase64(Url $url)
    {
        $pieces = explode(';', $url->getContent(), 2);

        if ((count($pieces) !== 2) || (strpos($pieces[0], 'image/') === false) || (strpos($pieces[1], 'base64,') !== 0)) {
            return false;
        }

        if (($info = @getimagesizefromstring(base64_decode(substr($pieces[1], 7)))) !== false) {
            return new self(
                $url,
                $url,
                200,
                $info['mime'],
                [$info[0], $info[1]],
                [],
                []
            );
        }
    }

    public function __construct(Url $startingUrl, Url $url, $statusCode, $contentType, $size, array $headers, array $info)
    {
        parent::__construct($startingUrl, $url, $statusCode, $contentType, $headers, $info);
        $this->size = $size;
    }

    /**
     * Get the image width.
     *
     * @return int
     */
    public function getWidth()
    {
        return (int) $this->size[0];
    }

    /**
     * Get the image height.
     *
     * @return int
     */
    public function getHeight()
    {
        return (int) $this->size[1];
    }
}


code:
<?php

namespace Embed\Http;

/**
 * Class to resolve some specific redirections.
 */
abstract class Redirects
{
    private static $patterns = [
        'google' => 'www.google.com/url*',
        'googleTranslator' => 'translate.google.com/translate',
        'hashBang' => '*#!*',
        'spotify' => 'play.spotify.com/*',
        'tumblr' => 't.umblr.com/redirect',
    ];

    /**
     * Resolve the url redirection.
     *
     * @param Url $url
     *
     * @return Url
     */
    public static function resolve(Url $url)
    {
        foreach (self::$patterns as $method => $pattern) {
            if ($url->match($pattern)) {
                return self::$method($url);
            }
        }

        return $url;
    }

    /**
     * Resolve a google redirection url.
     *
     * @param Url $url
     *
     * @return Url
     */
    public static function google(Url $url)
    {
        if (($value = $url->getQueryParameter('url'))) {
            return Url::create($value);
        }

        return $url;
    }

    /**
     * Resolve a google translation url.
     *
     * @param Url $url
     *
     * @return Url
     */
    public static function googleTranslator(Url $url)
    {
        if (($value = $url->getQueryParameter('u'))) {
            return Url::create($value);
        }

        return $url;
    }

    /**
     * Resolve an url with hashbang.
     *
     * @param Url $url
     *
     * @return Url
     */
    public static function hashBang(Url $url)
    {
        if (($path = preg_replace('|^(/?!)|', '', $url->getFragment()))) {
            return $url->withPath($url->getPath().$path);
        }

        return $url;
    }

    /**
     * Redirect the player of spotify.
     *
     * @param Url $url
     *
     * @return Url
     */
    public static function spotify(Url $url)
    {
        return $url->withHost('open.spotify.com');
    }

    /**
     * Redirect the tumblr url
     *
     * @param Url $url
     *
     * @return Url
     */
    public static function tumblr(Url $url)
    {
        if (($value = $url->getQueryParameter('z'))) {
            return Url::create($value);
        }

        return $url;
    }
}


code:
<?php

namespace Embed\Http;

use Embed\Utils;
use Exception;
use DOMDocument;
use SimpleXMLElement;

/**
 * Class to consume http responses.
 */
class Response extends AbstractResponse
{
    protected $content;
    protected $xmlContent;
    protected $jsonContent;
    protected $htmlContent;

    /**
     * Response constructor.
     *
     * @param Url $startingUrl
     * @param Url $url
     * @param int $statusCode
     * @param string|null $contentType
     * @param string $content
     * @param array $headers
     * @param array $info
     */
    public function __construct(Url $startingUrl, Url $url, $statusCode, $contentType, $content, array $headers, array $info = [])
    {
        parent::__construct($startingUrl, $url, $statusCode, $contentType, $headers, $info);
        $this->setContent($content);
    }

    /**
     * Get the content.
     *
     * @return string|null
     */
    public function getContent()
    {
        return $this->content;
    }

    /**
     * Get the content as HTML.
     *
     * @return DOMDocument|false
     */
    public function getHtmlContent()
    {
        if ($this->htmlContent === null) {
            try {
                if (empty($content = $this->content)) {
                    return $this->htmlContent = false;
                }

                $this->htmlContent = Utils::parse($content);
            } catch (Exception $exception) {
                return $this->htmlContent = false;
            }
        }

        return $this->htmlContent;
    }

    /**
     * Get the content as an array from JSON.
     *
     * @return array|false
     */
    public function getJsonContent()
    {
        if ($this->jsonContent === null) {
            try {
                if (($content = $this->content) === '') {
                    return $this->jsonContent = false;
                }

                $this->jsonContent = json_decode($content, true);
            } catch (\Exception $exception) {
                return $this->jsonContent = false;
            }
        }

        return $this->jsonContent;
    }

    /**
     * Get the content as XML.
     *
     * @return SimpleXMLElement|false
     */
    public function getXMLContent()
    {
        if ($this->xmlContent === null) {
            try {
                if (($content = $this->content) === '') {
                    return $this->xmlContent = false;
                }

                $errors = libxml_use_internal_errors(true);
                $this->xmlContent = new SimpleXMLElement($content);
                libxml_use_internal_errors($errors);
            } catch (Exception $exception) {
                return $this->xmlContent = false;
            }
        }

        return $this->xmlContent;
    }

    /**
     * Set the response content.
     *
     * @param string $content
     */
    private function setContent($content)
    {
        $this->content = $content;

        if (empty($this->contentType)) {
            return;
        }

        //Remove charset from Content-Type
        if (strpos($this->contentType, ';') !== false) {
            list($mime, $charset) = array_map('trim', explode(';', $this->contentType));

            $this->contentType = $mime;
            $charset = str_replace(['"', "'"], '', substr(strstr($charset, '='), 1));
            $this->content = Utils::toUtf8($content, trim($charset));
        }
    }
}