3.1. Adapter

3.1.1. Intent

According to the Gang of Four, the Adapter pattern is a way to “convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.” (Design Patterns: Elements of Reusable Object-Oriented Software, 2013, p. 139).

3.1.2. When to use it?

Use the Adapter pattern if you are are concerned about the integration of incompatible components / libraries, or if you face an existing class that doesn’t match the interface you need.

Note that the pattern also allows to decouple the client code from the adaptee implementations.

3.1.3. Diagram

Created using PhpStorm and yFiles.

Class diagram of the Adapter pattern

3.1.4. Implementation

FirstWeatherApi.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

namespace Phpatterns\Structural\Adapter\FirstWeatherApi;

class FirstWeatherApi
{
    /**
     * Get the weather main metrics for a given city.
     * @param string $city
     * @return array
     *  array['temp']       Temperature (°C)
     *  array['pressure']   Atmospheric pressure (hPa)
     *  array['humidity']   Humidity (%)
     *  array['temp_min']   Minimum temperature at the moment (°C)
     *  array['temp_max']   Maximum temperature at the moment (°C)
     */
    public function getMainMetrics($city)
    {
        // ..
        // Do the necessary to call the first Weather API and get data for the given city.
        // ..

        return [
            'temp' => 10.5,
            'pressure' => 1016.8,
            'humidity' => 91,
            'temp_min' => 8.3,
            'temp_max' => 12.0,
        ];
    }
}

SecondWeatherApi.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?php

namespace Phpatterns\Structural\Adapter\SecondWeatherApi;

class SecondWeatherApi
{
    /** @var \SimpleXMLElement */
    private $weatherData = null;

    /**
     * Get the weather data for a given city.
     * @param string $city
     */
    public function loadWeatherData($city)
    {
        $id = $this->getIDFromCity($city);

        // ...
        // Do the necessary to call the second Weather API and get data for the given city ID.
        // ...

        $xmlResponse = <<<'EOD'
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<rss version="2.0" xmlns:yweather="http://xml.weather.yahoo.com/ns/rss/1.0">
    <channel>
        <title>Weather - Paris, FR</title>
        <yweather:atmosphere humidity="93"  visibility="9.99"  pressure="1015.92"  rising="2" />
        <item>
            <title>Conditions for Paris, FR at 10:30 am CET</title>
            <yweather:condition  text="Partly Cloudy"  temp="9"  date="Sat, 26 Dec 2015 10:30 am CET" />
        </item>
    </channel>
</rss>
EOD;

        $this->weatherData = simplexml_load_string($xmlResponse);
    }

    /**
     * Retrieve ID for a city.
     * (Example : Spatial entities provided by Yahoo! GeoPlanet are referenced by a 32-bit
     * identifier: the Where On Earth ID (WOEID))
     * Have a look at: https://developer.yahoo.com/geo/geoplanet
     * @param $city
     * @return int
     */
    private function getIDFromCity($city)
    {
        // ...
        // use Yahoo! GeoPlanet API (or another one) to retrieve the ID for $city
        // ...

        // ID set manually to Paris - France for the example
        $id = 615702;
        return $id;
    }

    /**
     * Get the temperature for the loaded city
     * @return float | null if no data
     */
    public function getTemperature()
    {
        if ($this->weatherData) {
            $conditions = $this->weatherData->channel->item->xpath('yweather:condition');
            return (float) $conditions[0]->attributes()->temp;
        }

        return null;
    }

    /**
     * Get the humidity for the loaded city
     * @return float | null if no data
     */
    public function getHumidity()
    {
        if ($this->weatherData) {
            $atmosphere = $this->weatherData->channel->xpath('yweather:atmosphere');
            return (float) $atmosphere[0]->attributes()->humidity;
        }

        return null;
    }

    /**
     * Get the pressure for the loaded city
     * @return float | null if no data
     */
    public function getPressure()
    {
        if ($this->weatherData) {
            $atmosphere = $this->weatherData->channel->xpath('yweather:atmosphere');
            return (float) $atmosphere[0]->attributes()->pressure;
        }

        return null;
    }
}

WeatherInterface.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<?php

namespace Phpatterns\Structural\Adapter;

interface WeatherInterface
{
    /**
     * Get the weather for a given city (temperature, humidity and pressure only).
     * @param string $city
     * @return Weather
     */
    public function getWeather($city);
}

FirstWeatherApiAdapter.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

namespace Phpatterns\Structural\Adapter\FirstWeatherApi;

use Phpatterns\Structural\Adapter;

class FirstWeatherApiAdapter implements Adapter\WeatherInterface
{
    /** @var FirstWeatherApi */
    private $firstWeatherApi;

    public function __construct(FirstWeatherApi $firstWeatherApi)
    {
        $this->firstWeatherApi = $firstWeatherApi;
    }

    /**
     * Get the weather for a given city.
     * @param string $city
     * @return Adapter\Weather
     */
    public function getWeather($city)
    {
        $mainMetrics = $this->firstWeatherApi->getMainMetrics($city);

        return new Adapter\Weather(
            (float) $mainMetrics['temp'],
            (float) $mainMetrics['humidity'],
            (float) $mainMetrics['pressure']
        );
    }
}

SecondWeatherApiAdapter.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php

namespace Phpatterns\Structural\Adapter\SecondWeatherApi;

use Phpatterns\Structural\Adapter;

class SecondWeatherApiAdapter implements Adapter\WeatherInterface
{
    /** @var SecondWeatherApi */
    private $secondWeatherApi;

    public function __construct(SecondWeatherApi $secondWeatherApi)
    {
        $this->secondWeatherApi = $secondWeatherApi;
    }

    /**
     * Get the weather for a given city.
     * @param string $city
     * @return Adapter\Weather
     */
    public function getWeather($city)
    {
        $this->secondWeatherApi->loadWeatherData($city);

        return new Adapter\Weather(
            $this->secondWeatherApi->getTemperature(),
            $this->secondWeatherApi->getHumidity(),
            $this->secondWeatherApi->getPressure()
        );
    }
}

Weather.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php

namespace Phpatterns\Structural\Adapter;

class Weather
{
    /** @var float */
    private $temperature;

    /** @var float */
    private $humidity;

    /** @var float */
    private $pressure;

    public function __construct($temperature, $humidity, $pressure)
    {
        $this->temperature = $temperature;
        $this->humidity = $humidity;
        $this->pressure = $pressure;
    }
}

3.1.5. Tests

AdapterTest.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php

namespace Test\Phpatterns\Structural\Adapter;

use Phpatterns\Structural\Adapter;
use Phpatterns\Structural\Adapter\FirstWeatherApi;
use Phpatterns\Structural\Adapter\SecondWeatherApi;

class AdapterTest extends \PHPUnit_Framework_TestCase
{
    /**
     * Testing the adapter mechanism
     * @param Adapter\WeatherInterface $weather
     * @dataProvider adapterProvider
     */
    public function testAdapterMechanism(Adapter\WeatherInterface $weather)
    {
        $this->assertInstanceOf(
            Adapter\Weather::class,
            $weather->getWeather('Paris')
        );
    }

    public function adapterProvider()
    {
        return [
            [
                new FirstWeatherApi\FirstWeatherApiAdapter(
                    new FirstWeatherApi\FirstWeatherApi()
                )
            ],
            [
                new SecondWeatherApi\SecondWeatherApiAdapter(
                    new SecondWeatherApi\SecondWeatherApi()
                )
            ]
        ];
    }
}