1<?php
2/**
3 * URL parser and mapper
4 *
5 * PHP version 5
6 *
7 * LICENSE:
8 *
9 * Copyright (c) 2006, Bertrand Mansion <golgote@mamasam.com>
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or without
13 * modification, are permitted provided that the following conditions
14 * are met:
15 *
16 *    * Redistributions of source code must retain the above copyright
17 *      notice, this list of conditions and the following disclaimer.
18 *    * Redistributions in binary form must reproduce the above copyright
19 *      notice, this list of conditions and the following disclaimer in the
20 *      documentation and/or other materials provided with the distribution.
21 *    * The names of the authors may not be used to endorse or promote products
22 *      derived from this software without specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
25 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
26 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
27 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
28 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
30 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
31 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
32 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
33 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
34 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
35 *
36 * @category   Net
37 * @package    Net_URL_Mapper
38 * @author     Bertrand Mansion <golgote@mamasam.com>
39 * @license    http://opensource.org/licenses/bsd-license.php New BSD License
40 * @version    CVS: $Id: Mapper.php 232857 2007-03-28 10:23:04Z mansion $
41 * @link       http://pear.php.net/package/Net_URL_Mapper
42 */
43
44require_once 'Net/URL/Mapper/Path.php';
45require_once 'Net/URL/Mapper/Exception.php';
46
47/**
48 * URL parser and mapper class
49 *
50 * This class takes an URL and a configuration and returns formatted data
51 * about the request according to a configuration parameter
52 *
53 * @category   Net
54 * @package    Net_URL_Mapper
55 * @author     Bertrand Mansion <golgote@mamasam.com>
56 * @version    Release: @package_version@
57 */
58class Net_URL_Mapper
59{
60    /**
61    * Array of Net_URL_Mapper instances
62    * @var array
63    */
64    private static $instances = array();
65
66    /**
67    * Mapped paths collection
68    * @var array
69    */
70    protected $paths = array();
71
72    /**
73    * Prefix used for url mapping
74    * @var string
75    */
76    protected $prefix = '';
77
78    /**
79    * Optional scriptname if mod_rewrite is not available
80    * @var string
81    */
82    protected $scriptname = '';
83
84    /**
85    * Mapper instance id
86    * @var string
87    */
88    protected $id = '__default__';
89
90    /**
91    * Class constructor
92    * Constructor is private, you should use getInstance() instead.
93    */
94    private function __construct() { }
95
96    /**
97    * Returns a singleton object corresponding to the requested instance id
98    * @param  string    Requested instance name
99    * @return Object    Net_URL_Mapper Singleton
100    */
101    public static function getInstance($id = '__default__')
102    {
103        if (!isset(self::$instances[$id])) {
104            $m = new Net_URL_Mapper();
105            $m->id = $id;
106            self::$instances[$id] = $m;
107        }
108        return self::$instances[$id];
109    }
110
111    /**
112    * Returns the instance id
113    * @return   string  Mapper instance id
114    */
115    public function getId()
116    {
117        return $this->id;
118    }
119
120    /**
121    * Parses a path and creates a connection
122    * @param    string  The path to connect
123    * @param    array   Default values for path parts
124    * @param    array   Regular expressions for path parts
125    * @return   object  Net_URL_Mapper_Path
126    */
127    public function connect($path, $defaults = array(), $rules = array())
128    {
129        $pathObj = new Net_URL_Mapper_Path($path, $defaults, $rules);
130        $this->addPath($pathObj);
131        return $pathObj;
132    }
133
134    /**
135    * Set the url prefix if needed
136    *
137    * Example: using the prefix to differenciate mapper instances
138    * <code>
139    * $fr = Net_URL_Mapper::getInstance('fr');
140    * $fr->setPrefix('/fr');
141    * $en = Net_URL_Mapper::getInstance('en');
142    * $en->setPrefix('/en');
143    * </code>
144    *
145    * @param    string  URL prefix
146    */
147    public function setPrefix($prefix)
148    {
149        $this->prefix = '/'.trim($prefix, '/');
150    }
151
152    /**
153    * Set the scriptname if mod_rewrite not available
154    *
155    * Example: will match and generate url like
156    * - index.php/view/product/1
157    * <code>
158    * $m = Net_URL_Mapper::getInstance();
159    * $m->setScriptname('index.php');
160    * </code>
161    * @param    string  URL prefix
162    */
163    public function setScriptname($scriptname)
164    {
165        $this->scriptname = $scriptname;
166    }
167
168    /**
169    * Will attempt to match an url with a defined path
170    *
171    * If an url corresponds to a path, the resulting values are returned
172    * in an array. If none is found, null is returned. In case an url is
173    * matched but its content doesn't validate the path rules, an exception is
174    * thrown.
175    *
176    * @param    string  URL
177    * @return   array|null   array if match found, null otherwise
178    * @throws   Net_URL_Mapper_InvalidException
179    */
180    public function match($url)
181    {
182        $nurl = '/'.trim($url, '/');
183
184        // Remove scriptname if needed
185
186        if (!empty($this->scriptname) &&
187            strpos($nurl, $this->scriptname) === 0) {
188            $nurl = substr($nurl, strlen($this->scriptname));
189            if (empty($nurl)) {
190                $nurl = '/';
191            }
192        }
193
194        // Remove prefix
195
196        if (!empty($this->prefix)) {
197            if (strpos($nurl, $this->prefix) !== 0) {
198                return null;
199            }
200            $nurl = substr($nurl, strlen($this->prefix));
201            if (empty($nurl)) {
202                $nurl = '/';
203            }
204        }
205
206        // Remove query string
207
208        if (($pos = strpos($nurl, '?')) !== false) {
209            $nurl = substr($nurl, 0, $pos);
210        }
211
212        $paths = array();
213        $values = null;
214
215        // Make a list of paths that conform to route format
216
217        foreach ($this->paths as $path) {
218            $regex = $path->getFormat();
219            if (preg_match($regex, $nurl)) {
220                $paths[] = $path;
221            }
222        }
223
224        // Make sure one of the paths found is valid
225
226        foreach ($paths as $path) {
227            $regex = $path->getRule();
228            if (preg_match($regex, $nurl, $matches)) {
229                $values = $path->getDefaults();
230                array_shift($matches);
231                $clean = array();
232                foreach ($matches as $k => $v) {
233                    $v = trim($v, '/');
234                    if (!is_int($k) && $v !== '') {
235                        $values[$k] = $v;
236                    }
237                }
238                break;
239            }
240        }
241
242        // A path conforms but does not validate
243
244        if (is_null($values) && !empty($paths)) {
245            $e = new Net_URL_Mapper_InvalidException('A path was found but is invalid.');
246            $e->setPath($paths[0]);
247            $e->setUrl($url);
248            throw $e;
249        }
250
251        return $values;
252    }
253
254    /**
255    * Generate an url based on given parameters
256    *
257    * Will attempt to find a path definition that matches the given parameters and
258    * will generate an url based on this path.
259    *
260    * @param    array   Values to be used for the url generation
261    * @param    array   Key/value pairs for query string if needed
262    * @param    string  Anchor (fragment) if needed
263    * @return   string|false    String if a rule was found, false otherwise
264    */
265    public function generate($values = array(), $qstring = array(), $anchor = '')
266    {
267        // Use root path if any
268
269        if (empty($values) && isset($this->paths['/'])) {
270            return $this->scriptname.$this->prefix.$this->paths['/']->generate($values, $qstring, $anchor);
271        }
272
273        foreach ($this->paths as $path) {
274            $set = array();
275            foreach ($values as $k => $v) {
276                if ($path->hasKey($k, $v)) {
277                    $set[$k] = $v;
278                }
279            }
280
281            if (count($set) == count($values) &&
282                count($set) <= $path->getMaxKeys()) {
283
284                $req = $path->getRequired();
285                if (count(array_intersect(array_keys($set), $req)) != count($req)) {
286                    continue;
287                }
288                $gen = $path->generate($set, $qstring, $anchor);
289                return $this->scriptname.$this->prefix.$gen;
290            }
291        }
292        return false;
293    }
294
295    /**
296    * Returns defined paths
297    * @return array     Array of paths
298    */
299    public function getPaths()
300    {
301        return $this->paths;
302    }
303
304    /**
305    * Reset all paths
306    * This is probably only useful for testing
307    */
308    public function reset()
309    {
310        $this->paths = array();
311        $this->prefix = '';
312    }
313
314    /**
315    * Add a new path to the mapper
316    * @param object     Net_URL_Mapper_Path object
317    */
318    public function addPath(Net_URL_Mapper_Path $path)
319    {
320        $this->paths[$path->getPath()] = $path;
321    }
322
323}
324?>