Skip to content

Instantly share code, notes, and snippets.

@mneuhaus
Last active August 29, 2015 14:03
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mneuhaus/586cb19bd9e856af9afa to your computer and use it in GitHub Desktop.
Save mneuhaus/586cb19bd9e856af9afa to your computer and use it in GitHub Desktop.
Keep it simple stupid :)

Status of Expose

Currently i'd say that Expose is feature-wise at above 90% complete. There is little to no documentation currently aside from a little demo package i did to test + maintain it. One reason i didn't yet write documentation or shouted out to other people that they should use expose is my feeling, that we took a wrong direction. The introduction of TYPO3.Form and TYPO3.TypoScript added a flexibilty to the whole thing that you can't really find in other "admins". But it adds a huge amount of complexity. After using it extensively for 2 Customer projects this complexity feels plain wrong. It seems the direction expose went, got it to probably 70-80% on a DRY scale, yet we dropped well below 30% on a KISS scale. The performance was an issue as well, which is improved through the TypoScript caching, yet that's one more thing that you need to configure and take care of :/.

Because of this i've had a brewing idea in my head for a while:

  • remove TYPO3.TypoScript
  • remove TYPO3.Form
  • simple inheritance + fallback based controllers
  • viewHelpers to make forms concise and easy to maintain
  • processViewHelper to easily filter, sort, paginate, limit tables/lists without complex widgets

I did a little prototype the last few days, it's quite rough in some places of course, but basically i've got a complete simple crud workflow working already :) https://github.com/mneuhaus/Famelo.Components

Template-Fallback for ControllerInheritance

Currently, if you inherit from a controller that contains actions you need to copy over every template for that actions to your inheriting controller to use them. Imho the view of the controller should follow the same principles as the controller inheritance itself. E.g. use the template/layout/partials from the inherited controller, if it's not overridden in the extending controller.

Usecase

Given you have an CrudController with an action "index" and an template inside "Templates/Crud/Index.html".
When you create a "PostController" that extends from that controller.
Then it should use the template "Templates/Crud/Index.html" for the index action.

When you create a template "Templates/Post/Index.html"
Then it should use the template "Templates/Post/Index.html" for the index action.

Example

This way we can create a simple, easily extendable base CrudController that you can use like this:

class ProjectController extends CrudController {
	/**
	 * @var string
	 */
	protected $entity = '\Famelo\George\Domain\Model\Project';

	/**
	 * @var string
	 */
	protected $listFields = array('name', 'branches', 'gerritProject');

	/**
	 * @var array
	 */
	protected $filterFields = array('active');

	/**
	 * @var string
	 */
	protected $defaultSortBy = 'name';
}

It will automatically use the CrudController templates and you have a working crud for your entity in no time.

Screenshots

Form/FieldViewHelper

One general hassle the TYPO3.Form package tries to solve is the easy creation of forms, which it does pretty good for static forms. But for Expose this seems to be one more abstraction that adds complexity, performance overhead and quite some code to actually make that work together. And if you want to create a new form control you have to Add it to the Settings.yaml, create a template for it and configure that template to be used in the Schema.ts. After using Expose in customer projects this gets a nasty forest of configuration files that doesn't make any fun.

Instead i would proprose a new ViewHelper called "<f:form.field/>". This ViewHelper composes a lot of boilerplate code like this:

<div class="form-group">
	<label for="someProperty" class="col-sm-2 control-label">Some Property</label>
	<div class="col-sm-10">
		<f:form.texfield property="someProperty" id="someProperty" />
		<f:validation.results for="someProperty">
			<f:if condition="{validationResults.flattenedErrors}">
	 			<f:for each="{validationResults.errors}" as="error">
	 				<span class="help-block">' . $error->getMessage() . '</span>
	 			</f:for>
		 	</f:if>
		 </f:validation.results>
	</div>
</div>

Into this:

<c:form.field property="someProperty" label="Some Property" />

This ViewHelper does the following things:

  • select a Partial to render the form control based on the propertyType
  • wrap that rendered form control with the Default or specified wrap
  • inside that wrap you get property specific validation errors included

Example:

If the property "someProperty" is of type "string" it will choose the Partial "Form/Field/Textfield.html" by default to render that control. Since no specific wrap was specified for that field the control will then be rendered into a default wrap like for example for bootstrap.

Custom Control

If you specify an additional argument "control" for the fieldViewHelper like this:

<c:form.field property="someProperty" label="Some Property" control="SomePropertyTextfield" />

Then it will use a Partial called "Form/Field/SomePropertyTextfield.html" and render that into the default wrap.

Custom Control without additional partial

If you give that field some children like this:

<c:form.field property="someProperty" label="Some Property" control="SomePropertyTextfield">
... my custom form control ...
</f:form.field>

Then it will use that as control and wrap that with the default wrap.

Custom Wrap

If you specify an additional argument "wrap" for the fieldViewHelper like this:

<c:form.field property="someProperty" label="Some Property" wrap="Zurb" />

Then it will use the default Partial to render that control and put that into a wrap called "Form/Wrap/Zurb.html".

Bottom line

As you can see, this approach would reduce the boilerplate code needed for forms without giving up much flexibility. The CrudController can for the simple default case simple loop over all the entity properties like this:

<f:for each="{fields}" as="field">
	<c:form.field property="{field}"/>
</f:for>

And if you want to customize that it's still quite consice

<c:form.field property="foo"/>
<c:form.field property="bar"/>
<c:form.field property="guz"/>

ProcessViewHelper

One thing that the TYPO3.TypoScript introduction brought with it was the concept of processors to wrap tables/lists with things like sort, filter, search, paginate, etc. I think we could bring a easy version of this directly into TYPO3.Fluid like this:

<c:process objects="{entities}" processors="{listProcessors}">
	<div class="row">
		<div class="col-xs-9">
			<c:block name="top"/>
			<table class="table table-bordered table-striped">
				<tr>
					<f:for each="{listFields}" as="field">
						<th>
							<c:wrap name="field" arguments="{field: field}"><i class="fa"></i> {field -> c:format.humanizeCamelCase()}</c:wrap>
						</th>
					</f:for>
					<th></th>
				</tr>
				
				<f:for each="{entities}" as="entity">
					<tr>
						<f:for each="{listFields}" as="field">
							<td>
								<c:property object="{entity}" name="{field}" />
							</td>
						</f:for>
						<td class="actions">
							<f:render partial="Table/Actions" arguments="{entity: entity}" />
						</td>
					<tr>
				</f:for>
			</table>
			<c:block name="bottom" />
		</div>
		<div class="col-xs-3">
			<c:block name="sidebar" />
		</div>
	</div>
</c:process>

ProcessViewHelper

This ViewHelper takes 2 arguments:

  • an QueryResult into objects
  • list of Processors to apply

The ProcessViewHelper loops over every specified Processor and invokes a "process($query)" method. The Process itself has access to the current "viewHelperVariableContainer" and can add content to a "Block" or add an callback for a "Wrapper".

Examples:

PaginationProcessor

  • The pagination processor updates the query to the current limit + page
  • renders a partial that contains the pagination itself
  • adds that rendered pagination to the block "bottom"
  • the "<c:block name="bottom" />" viewHelper takes that content and puts it into the specified place in the template
	public function process($query) {
		$this->query = $query;
		$this->request = $this->controllerContext->getRequest();

		$this->total = $this->query->count();
		$limits = $this->handleLimits();
		$pagination = $this->handlePagination();

		$content = $this->viewHelperVariableContainer->getView()->renderPartial('Pagination', NULL, array(
			'pagination' => $pagination,
			'limits' => $limits
		));
		$this->addToBlock('bottom', $content);
	}

SortProcessor

  • The sort processor updates the query to the current sortBy and order
  • adds a wrapper callback for the wrapper "field"
  • gets called by the "<c:wrap name="field" arguments="{field: field}">" to add the sort wrapper
	public function process($query) {
		$this->handleSorting($query;
		$this->addWrapper('field', $this);
	}

	public function wrap($content, $arguments) {
		... wrap the content with a some sort link and direction classes ...
		return $content;
	}

Bottom line

You're probably now thinking, wtf, we won't he shut up and use Widgets +/ TypoScript? Well, take a look at the code you need to write + maintain to have this working this way, vs Widgets +/ TypoScript. If you add PartialInheritance from "2_ControllerInheritanceFalbacks.md" into this mix you can easily override anything you want with very concise and clean code/templates. :)

@dogawaf
Copy link

dogawaf commented Jul 3, 2014

I like it :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment