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