Using a DTO for routing parameter

Hi :slight_smile: Back from vacation, working on some nice routing for a report tool. I got a Query DTO object, that is passed as a argument. It has a property named “routingParameters” that I’m using in my Routes.yaml like this

-
  name: 'report'
  uriPattern: 'reports/{query}'
  defaults:
    '@controller': 'Administration\Report'
    '@action': 'search'
  routeParts:
    query:
      objectType: 'Vendor\App\Domain\Dto\Query'
      uriPattern: '{routingParameters.domains}/{routingParameters.dateRangeStart}/{routingParameters.dateRangeEnd}'

But the routing looks at it as a persisted object, so I get this expection

Tried to convert an object of type "Vendor\App\Domain\Dto\Query" to an identity array, but it is unknown to the Persistence Manager.

It’s not meant to be peristed, as it’s a link to a report like this /admin/reports/domain1,domain2,domain3/startdate/enddate/

How should this be accomplished?

1 Like

You need a custom route part handler for this. if you give it objectType it expects an entity.

I figured that out, and started a exciting journey into the RoutePartHandler thing - and i came out alive :smiley:

<?php

namespace Vendor\App\Routing;

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Mvc\Routing\DynamicRoutePart;
use Vendor\App\Domain\Dto\Query;
use Vendor\App\Domain\Model\Domain;

/**
 * Reporting Route Part
 *
 * @api
 */
class ReportRoutePartHandler extends DynamicRoutePart
{


	const REPORT_PATH_MATCHER = '/(?P<domains>[^\/]*)?\/?(?P<dateRangeStart>\d\d-\d\d-\d{4})?\/?(?P<dateRangeEnd>\d\d-\d\d-\d{4})?\/?(?:(?P<sortingKey>.*):(?P<sortingValue>.*))?/';

	/**
	 * Extracts the node path from the request path.
	 *
	 * @param string $requestPath The request path to be matched
	 * @return string value to match, or an empty string if $requestPath is empty or split string was not found
	 */
	protected function findValueToMatch($requestPath)
	{
		if ($this->splitString !== '') {
			$splitStringPosition = strpos($requestPath, $this->splitString);
			if ($splitStringPosition !== false) {
				return substr($requestPath, 0, $splitStringPosition);
			}
		}

		return $requestPath;
	}

	/**
	 * Checks whether the current URI section matches the configured RegEx pattern.
	 *
	 * @param string $requestPath value to match, the string to be checked
	 * @return boolean TRUE if value could be matched successfully, otherwise FALSE.
	 */
	protected function matchValue($requestPath) {

		preg_match(self::REPORT_PATH_MATCHER, $requestPath, $matches);

		if (!isset($matches['domains']) || strlen($matches['domains']) === 0) {
			return false;
		}

		//$query = new Query();

		$query = [];

		$query['domains'] = explode(',', $matches['domains']);

		if (isset($matches['dateRangeStart'])) {
			$query['dateRangeStart'] = $matches['dateRangeStart']; #\DateTime::createFromFormat('d-m-Y', $matches['dateRangeStart']);
		}

		if (isset($matches['dateRangeEnd'])) {
			$query['dateRangeEnd'] = $matches['dateRangeEnd']; #\DateTime::createFromFormat('d-m-Y', $matches['dateRangeEnd']);
		}

		$this->value = $query;

		return TRUE;
	}

	/**
	 * Checks whether the route part matches the configured RegEx pattern.
	 *
	 * @param Query $value
	 * @return boolean TRUE if value could be resolved successfully, otherwise FALSE.
	 */
	protected function resolveValue($value) {
		if (!($value instanceof Query)) {
			return FALSE;
		}

		$this->value = implode('/', $value->getRoutingParameters());

		return TRUE;
	}
}

This gave me routes like

admin/reports/www.domain1.dk,www.domain2.dk,www.domain3.dk,www.domain4.dk/13-03-2017/14-07-2017

which was automatically mapped to the object in my action methods parameter :thumbsup:

2 Likes

Another detail, to show how nicely everything is tied together:

I have a DomainTypeConverter that can take string of a domain and find a corresponding Domain object.

So in my searchAction that looks like this

	/**
	 * @param Query $query
	 */
	public function searchAction(Query $query) {
		$results = $this->pixelRepository->findByQuery($query);
		$this->view->assign('results', $results);
		$this->view->assign('report', $query);
	}

The corresponding Query object has a property like this

	/**
	 * @var Collection<Domain>
	 */
	protected $domains;

This property is filled with Domain objects made from the comma-seperated-list since there is a CollectionTypeConverter that can take a comma-separated (made in my route part handler) list and turn it into a Collection :slight_smile: :trophy: