awesomite/chariot

Parameter Providers

Closed this issue · 10 comments

I'm not sure how possible this would be with the architecture, but one thing that seems a bit verbose is the providing of parameters for URL generation. In other routing libraries I've worked on, it is possible to hand off a parameter provider to the URL generation:

$router->linkTo('showArticle')->withProvider($article)

The provider would implement a single interface with a method like getParameter($name) wherein the URL for the action would be parsed for the parameters required, and call that method on the provider for each, so let's say you have:

/articles/{{ category }}/{{ id }}-{{ slug }}

This would call getParameter('category') for which the provider would give the value then getParameter('id') for which the provider would give the value and finally getParameter('slug') for which the provider would give the value. This would be equivalent to calling withParameters() and providing an array, or three separate calls to withParameter(). All transformations would still apply as if those were the exact values provided to each of the existing calls.

This helps keep things brief and really helps with generating routes in views/templates.

Thoughts?

I have one question.

$router->get('/articles/{{ category }}/{{ id }}-{{ slug }}', 'showArticle');

$provider = new ArrayProvider([
    'id' => 5,
    'category' => 'books',
    'slug' => 'something',
    'foo' => 'bar'
]);

echo $router->linkTo('showArticle')->withProvider($provider);

What is expected output in this case? /articles/books/5-something or /articles/books/5-something?foo=bar?

Using a provider does not generally enable you to add ?foo=bar extra parameters. Through the provider interface the params are only called according to what is needed by the URL. So params are requested from the provider by the link. Note, that this should also not prevent additional calls to add parameters as normal. So any additional stuff that normally ends up in a query can be added via the existing calls.

Ok, I understand. Can you please share a real example where you want to use such functionality? It would be nice to have this for documentation. I am thinking - maybe it would be better to have
a method setProvider directly in the router, e.g.:

$router->get('/articles/{{ category }}/{{ id }}', 'showArticle');
$provider = new ArrayProvider([
    'category' = => 'books',
]);
$router->setProvider($provider);
echo $router->linkTo('showArticle')->withParam('id', 5);
// /articles/books/5

Maybe a provider should have an access to routeName and defined properties?

class CannotFetchException extends \Exception
{
}

interface ProviderInterface
{
    /**
     * @param string $routeName
     * @param array  $params
     * @return string
     * 
     * @throws CannotFetchException
     */
    public function getValue(string $paramName, string $routeName, array $params);
}

class CategoryForArticleProvider implements ProviderInterface
{
    private $categories;
    
    public function __construct($categories)
    {
        $this->categories;
    }

    public function getValue(string $paramName, string $routeName, array $params)
    {
        if ('id' !== $paramName || 'showArticle' !== $routeName || !array_key_exists('id', $params)) {
            $this->throwCannotFetch();
        }
        
        return $this->categories->findByArticle($params['id'])->getName();
    }
    
    private function throwCannotFetch()
    {
        throw new CannotFetchException();
    }
}

@mattsah what do you think?

In real world examples, I implement the interface often a model/entity which uses a trait. So the aforementioned example is quite accurate to the actual use case. So generally each entity/model is considered a provider, and could be passed for acquiring parameter values. That said, I'm not opposed and having considered more last night, to the notion that there could (and in this latest case) probably could be a middleman.

The only reason for this however is because I will need an inflector to transform the requested parameter into an appropriate property name to call on the entity since my params are $under_score and properties are $camelCase.

From my other routing, here's the trait:

<?php

/**
 *
 */
trait ParamProvider
{
	/**
	 * Get the value of a route parameter given the name of the parameter
	 *
	 * @access public
	 * @param string $name The name of the parameter for which to get a value
	 * @return mixed The value of the route parameter
	 */
	public function getRouteParameter($name)
	{
		$name = ucfirst($name);
		return $this->{ 'get' . $name }();
	}
}

you can see here this would actually fail on a proprety with an underscore, because it would call something like getSome_property if the parameter name was some_proprety. That method is an implementation of the interface. That trait is then added to a model such as:

<?php
use iMarc\App;

/**
 * News Item Model
 *
 */
class NewsItem extends App\Record implements Journey\ParamProvider
{
	use ParamProvider;

	public function __toString()
	{
		return $this->getTitle();
	}


	/**
	 *
	 */
	public function makeCategoryList()
	{
		$list = array();

		foreach ($this->buildNewsCategories() as $category) {
			$list[] = $category->getName();
		}

		return $list;
	}
}

Then each model can be passed to the same route to generate specific routes for their parameters, for example in twig:

        {% if featured_items|length %}
            <div class="listing">
                <ul>
                    {% for item in featured_items %}
                        <li class="featured">
                            <h2 class="title">
                                {% if item.getUrl()|length %}
                                    <a href="{{ item.getUrl() }}">
                                {% else %}
                                    <a href="{{ router.link('/news/articles/{id}:{title:!}', item) }}">
                                {% endif %}
                                    Featured: {{ item.getTitle() }}
                                </a>
                            </h2>
                            <div class="meta">
                                {{ item.getDisplayDate()|date('F j, Y') }}
                            </div>
                            <div class="summary">
                                <p>
                                    {{ item.getShortDescription() }}
                                </p>
                            </div>
                        </li>
                    {% endfor %}
                </ul>
            </div>
        {% endif %}

See my last comment for example of current use case and implementation. I'm looking to use chariot more because of reverse routing, as you can see from the twig, link generation on my current router doesn't do reverse routing, but just does reverse parameter placement.

Using your interface should be OK, although I'm not sure ideal. I'm suspecting you have it on the router because the link object doesn't actually have access to the routing information proper? Would it make sense for $router->linkTo() to pass itself to the link so the link could access the router as needed in these respects?

Apologies I've not looked at the actual code or taken a shot at this myself. I was mostly just looking to get some initial interest gauging.

My approach with the interface you're suggesting would probably be something along the lines of a provider and a separate helper:

<?php

use Awesomite\Chariot\Pattern\PatternRouter;

class AccessMethodProvider
{
	public function __construct(Inflector $inflector)
	{
		$this->inflector;
	}


	public function setEntity($entity)
	{
		$this->entity = $entity;
	}


	public function getValue(string $param_name, string $route_name, array $params)
	{
		return $this->entity->{ 'get' . ucfirst($htis->inflector->camelize($param_name)) }();
	}
}




class Anchor
{
	public function __construct(PatternRouter $router, AccessMethodProvider $provider)
	{
		$this->router   = $router;
		$this->provider = $provider;

		$this->router->setProvider($provider);
	}


	public function __invoke($action, $entity)
	{
		$this->provider->setEntity($entity);

		return $this->router->linkTo($action);
	}
}

Then I could just register the helper in the twig globals and call something like:

<a href="{{ anchor('viewArticle', article) }}">{{ artitcle.title }}</a>

Sorry, one more post. Alternatively, would it just be possible to expose a method to get parameters required by a reverse route? then i wouldn't even need the interface, cause I could just make the helper and use withParam():

class Anchor
{
	public function __construct(PatternRouter $router, Inflector $inflector)
	{
		$this->router    = $router;
		$this->inflector = $inflector;
	}


	public function __invoke($action, $entity)
	{
		$link = $this->router->linkTo($action);

		foreach ($this->router->getParameters($action) as $param) {
			$value = $this->entity->{ 'get' . ucfirst($htis->inflector->camelize($param)) }();

			if ($value !== NULL) {
				$link->withParam($param, $value);
			}
		}

		return $link;
	}
}

@mattsah ok, I'll think how to handle this, then let you know

Parameters providers have been added in v0.4.0

Looks like this will do what I need. Thanks for all the support. It's unfortunate this project doesn't get as much attention as it should. It's definitely the most robust router out there at this point. As soon as I finalize my package for my framework, I'll be using it quite extensively.