1<?php
2
3namespace Negotiation;
4
5use Negotiation\Exception\InvalidArgument;
6use Negotiation\Exception\InvalidHeader;
7
8abstract class AbstractNegotiator
9{
10    /**
11     * @param string $header     A string containing an `Accept|Accept-*` header.
12     * @param array  $priorities A set of server priorities.
13     *
14     * @return AcceptHeader|null best matching type
15     */
16    public function getBest($header, array $priorities)
17    {
18        if (empty($priorities)) {
19            throw new InvalidArgument('A set of server priorities should be given.');
20        }
21
22        if (!$header) {
23            throw new InvalidArgument('The header string should not be empty.');
24        }
25
26        // Once upon a time, two `array_map` calls were sitting there, but for
27        // some reasons, they triggered `E_WARNING` time to time (because of
28        // PHP bug [55416](https://bugs.php.net/bug.php?id=55416). Now, they
29        // are gone.
30        // See: https://github.com/willdurand/Negotiation/issues/81
31        $acceptedHeaders = array();
32        foreach ($this->parseHeader($header) as $h) {
33            try {
34                $acceptedHeaders[] = $this->acceptFactory($h);
35            } catch (Exception\Exception $e) {
36                // silently skip in case of invalid headers coming in from a client
37            }
38        }
39        $acceptedPriorities = array();
40        foreach ($priorities as $p) {
41            $acceptedPriorities[] = $this->acceptFactory($p);
42        }
43        $matches         = $this->findMatches($acceptedHeaders, $acceptedPriorities);
44        $specificMatches = array_reduce($matches, 'Negotiation\Match::reduce', []);
45
46        usort($specificMatches, 'Negotiation\Match::compare');
47
48        $match = array_shift($specificMatches);
49
50        return null === $match ? null : $acceptedPriorities[$match->index];
51    }
52
53    /**
54     * @param string $header accept header part or server priority
55     *
56     * @return AcceptHeader Parsed header object
57     */
58    abstract protected function acceptFactory($header);
59
60    /**
61     * @param AcceptHeader $header
62     * @param AcceptHeader $priority
63     * @param integer      $index
64     *
65     * @return Match|null Headers matched
66     */
67    protected function match(AcceptHeader $header, AcceptHeader $priority, $index)
68    {
69        $ac = $header->getType();
70        $pc = $priority->getType();
71
72        $equal = !strcasecmp($ac, $pc);
73
74        if ($equal || $ac === '*') {
75            $score = 1 * $equal;
76
77            return new Match($header->getQuality() * $priority->getQuality(), $score, $index);
78        }
79
80        return null;
81    }
82
83    /**
84     * @param string $header A string that contains an `Accept*` header.
85     *
86     * @return AcceptHeader[]
87     */
88    private function parseHeader($header)
89    {
90        $res = preg_match_all('/(?:[^,"]*+(?:"[^"]*+")?)+[^,"]*+/', $header, $matches);
91
92        if (!$res) {
93            throw new InvalidHeader(sprintf('Failed to parse accept header: "%s"', $header));
94        }
95
96        return array_values(array_filter(array_map('trim', $matches[0])));
97    }
98
99    /**
100     * @param AcceptHeader[] $headerParts
101     * @param Priority[]     $priorities  Configured priorities
102     *
103     * @return Match[] Headers matched
104     */
105    private function findMatches(array $headerParts, array $priorities)
106    {
107        $matches = [];
108        foreach ($priorities as $index => $p) {
109            foreach ($headerParts as $h) {
110                if (null !== $match = $this->match($h, $p, $index)) {
111                    $matches[] = $match;
112                }
113            }
114        }
115
116        return $matches;
117    }
118}
119