Skip to content
This repository has been archived by the owner on Feb 26, 2018. It is now read-only.

Commit

Permalink
Merge pull request #92 from adamwathan/pr/91
Browse files Browse the repository at this point in the history
Support nested data in bound models
  • Loading branch information
adamwathan committed Mar 25, 2016
2 parents b95e672 + 8a7693d commit 5fc21cb
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 42 deletions.
12 changes: 6 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Boring name for a boring package. Builds form HTML with a fluent-ish, hopefully
- [Remembering Old Input](#remembering-old-input)
- [Error Messages](#error-messages)
- [CSRF Protection](#csrf-protection)
- [Model Binding](#model-binding)
- [Data Binding](#data-binding)

<a href="#installation"></a>
## Installation
Expand All @@ -24,7 +24,7 @@ composer require adamwathan/form

### Laravel

> This package works great as a replacement Form Builder that was removed in Laravel 5. The API is different but all of the features are there.
> This package works great as a replacement Form Builder that was removed in Laravel 5. The API is different but all of the features are there.
If you are using Laravel 4 or 5, you can register the FormServiceProvider to automatically gain access to the Old Input and Error Message functionality.

Expand Down Expand Up @@ -353,10 +353,10 @@ Assuming you set a CSRF token when instantiating the Formbuilder (or you are usi
<?= $builder->token(); ?>
```
<a href="#model-binding"></a>
## Model Binding
<a href="#data-binding"></a>
## Data Binding
Sometimes you might have a form where all of the fields match properties on some sort of object in your system, and you want the user to be able to edit those properties. Model binding makes this really easy by allowing you to bind a model to your form that will be used to automatically provide all of the default values for your fields.
Sometimes you might have a form where all of the fields match properties on some sort of object or array in your system, and you want the user to be able to edit that data. Data binding makes this really easy by allowing you to bind an object or array to your form that will be used to automatically provide all of the default values for your fields.
```php
$model->first_name = "John";
Expand All @@ -375,6 +375,6 @@ $model->date_of_birth = new DateTime('1985-05-06');
> This will work out of the box with Laravel's Eloquent models.

When using model binding, old input will still take priority over any values on your model, so you can still easily redirect the user back to the form with any validation errors without losing any of the data they entered.
When using data binding, old input will still take priority over any of your bound values, so you can still easily redirect the user back to the form with any validation errors without losing any of the data they entered.

> Note: Be sure to `bind` before creating any other form elements.
49 changes: 49 additions & 0 deletions src/AdamWathan/Form/Binding/BoundData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace AdamWathan\Form\Binding;

class BoundData
{
protected $data;

public function __construct($data)
{
$this->data = $data;
}

public function get($name, $default = null)
{
return $this->dotGet($this->transformKey($name), $default);
}

protected function dotGet($dotKey, $default)
{
$keyParts = array_filter(explode('.', $dotKey));

return $this->dataGet($this->data, $keyParts, $default);
}

protected function dataGet($target, $keyParts, $default)
{
if (count($keyParts) == 0) {
return $target;
}

$key = array_shift($keyParts);

if (is_array($target) && isset($target[$key])) {
return $this->dataGet($target[$key], $keyParts, $default);
}

if (property_exists($target, $key) || method_exists($target, '__get')) {
return $this->dataGet($target->{$key}, $keyParts, $default);
}

return $default;
}

protected function transformKey($key)
{
return str_replace(['[]', '[', ']'], ['', '.', ''], $key);
}
}
40 changes: 14 additions & 26 deletions src/AdamWathan/Form/FormBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace AdamWathan\Form;

use AdamWathan\Form\Binding\BoundData;
use AdamWathan\Form\Elements\Button;
use AdamWathan\Form\Elements\Checkbox;
use AdamWathan\Form\Elements\Date;
Expand All @@ -26,7 +27,7 @@ class FormBuilder

protected $csrfToken;

protected $model;
protected $boundData;

public function setOldInputProvider(OldInputInterface $oldInputProvider)
{
Expand Down Expand Up @@ -61,7 +62,7 @@ protected function hasToken()

public function close()
{
$this->unbindModel();
$this->unbindData();

return '</form>';
}
Expand Down Expand Up @@ -218,9 +219,9 @@ public function getError($name, $format = null)
return $message;
}

public function bind($model)
public function bind($data)
{
$this->model = is_array($model) ? (object) $model : $model;
$this->boundData = new BoundData($data);
}

public function getValueFor($name)
Expand All @@ -229,8 +230,8 @@ public function getValueFor($name)
return $this->getOldInput($name);
}

if ($this->hasModelValue($name)) {
return $this->getModelValue($name);
if ($this->hasBoundData()) {
return $this->getBoundValue($name, null);
}

return null;
Expand All @@ -250,36 +251,28 @@ protected function getOldInput($name)
return $this->escape($this->oldInput->getOldInput($name));
}

protected function hasModelValue($name)
protected function hasBoundData()
{
if (! isset($this->model)) {
return false;
}

$name = $this->transformKey($name);

return isset($this->model->{$name}) || method_exists($this->model, '__get');
return isset($this->boundData);
}

protected function getModelValue($name)
protected function getBoundValue($name, $default)
{
$name = $this->transformKey($name);

return $this->escape($this->model->{$name});
return $this->escape($this->boundData->get($name, $default));
}

protected function escape($value)
{
if (!is_string($value)) {
if (! is_string($value)) {
return $value;
}

return htmlentities($value, ENT_QUOTES, 'UTF-8');
}

protected function unbindModel()
protected function unbindData()
{
$this->model = null;
$this->boundData = null;
}

public function selectMonth($name)
Expand All @@ -301,9 +294,4 @@ public function selectMonth($name)

return $this->select($name, $options);
}

protected function transformKey($key)
{
return str_replace('[]', '', $key);
}
}
115 changes: 105 additions & 10 deletions tests/BindingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,22 +136,95 @@ public function testBindMagicProperty()
$object = new MagicGetter;
$this->form->bind($object);

$expected = '<input type="text" name="not_set" value="foo">';
$result = (string) $this->form->text('not_set');
$expected = '<input type="text" name="not_magic" value="foo">';
$result = (string) $this->form->text('not_magic');
$this->assertEquals($expected, $result);

$expected = '<input type="text" name="magic" value="bar">';
$result = (string) $this->form->text('magic');
$this->assertEquals($expected, $result);
}

public function testBindArray()
{
$model = ['first_name' => 'John'];
$this->form->bind($model);
$array = ['first_name' => 'John'];
$this->form->bind($array);

$expected = '<input type="text" name="first_name" value="John">';
$result = (string) $this->form->text('first_name');
$this->assertEquals($expected, $result);
}

public function testCloseUnbindsModel()
public function testBindNestedArray()
{
$array = [
'address' => [
'city' => 'Roswell',
'tree' => [
'has' => [
'nested' => 'Bird'
]
],
],
];
$this->form->bind($array);

$expected = '<input type="text" name="address[city]" value="Roswell">';
$result = (string) $this->form->text('address[city]');
$this->assertEquals($expected, $result);

$expected = '<input type="text" name="address[tree][has][nested]" value="Bird">';
$result = (string) $this->form->text('address[tree][has][nested]');
$this->assertEquals($expected, $result);
}

public function testBindNestedObject()
{
$object = json_decode(json_encode([
'address' => [
'city' => 'Roswell',
'tree' => [
'has' => [
'nested' => 'Bird'
]
],
],
]));
$this->form->bind($object);

$expected = '<input type="text" name="address[city]" value="Roswell">';
$result = (string) $this->form->text('address[city]');
$this->assertEquals($expected, $result);

$expected = '<input type="text" name="address[tree][has][nested]" value="Bird">';
$result = (string) $this->form->text('address[tree][has][nested]');
$this->assertEquals($expected, $result);
}

public function testBindNestedMixed()
{
$object = [
'address' => [
'city' => 'Roswell',
'tree' => json_decode(json_encode([
'has' => [
'nested' => 'Bird'
]
])),
],
];
$this->form->bind($object);

$expected = '<input type="text" name="address[city]" value="Roswell">';
$result = (string) $this->form->text('address[city]');
$this->assertEquals($expected, $result);

$expected = '<input type="text" name="address[tree][has][nested]" value="Bird">';
$result = (string) $this->form->text('address[tree][has][nested]');
$this->assertEquals($expected, $result);
}

public function testCloseUnbindsData()
{
$object = $this->getStubObject();
$this->form->bind($object);
Expand All @@ -162,7 +235,7 @@ public function testCloseUnbindsModel()
$this->assertEquals($expected, $result);
}

public function testAgainstXSSAttacksInBoundModels()
public function testAgainstXSSAttacksInBoundData()
{
$object = $this->getStubObject();
$object->first_name = '" onmouseover="alert(\'xss\')';
Expand All @@ -185,13 +258,32 @@ public function testValueTakesPrecedenceOverBinding()

public function testBindingOnCheckboxTakesPrecedenceOverDefaultToChecked()
{
$object = $this->getStubObject();
$object = (object) ['published' => 1];
$this->form->bind($object);

$expected = '<input type="checkbox" name="published[]" value="1" checked="checked">';
$expected .= '<input type="checkbox" name="published[]" value="0">';
$result = (string) $this->form->checkbox('published[]', 1);
$result .= (string) $this->form->checkbox('published[]', 0)->defaultToChecked();
$this->assertEquals($expected, $result);

$object = (object) ['published' => 0];
$this->form->bind($object);

$expected = '<input type="checkbox" name="published[]" value="1">';
$result = (string) $this->form->checkbox('published[]', 1)->defaultToChecked();
$this->assertEquals($expected, $result);

$object = (object) ['published' => true];
$this->form->bind($object);

$expected = '<input type="checkbox" name="published[]" value="1" checked="checked">';
$result = (string) $this->form->checkbox('published[]', 1);
$this->assertEquals($expected, $result);

$object = (object) ['published' => false];
$this->form->bind($object);

$expected = '<input type="checkbox" name="published[]" value="1">';
$result = (string) $this->form->checkbox('published[]', 1)->defaultToChecked();
$this->assertEquals($expected, $result);
}

Expand Down Expand Up @@ -300,15 +392,18 @@ private function getStubObject()
$obj->number = '0';
$obj->favourite_foods = ['fish', 'chips'];
$obj->published = '1';
$obj->private = false;

return $obj;
}
}

class MagicGetter
{
public $not_magic = 'foo';

public function __get($key)
{
return 'foo';
return 'bar';
}
}

0 comments on commit 5fc21cb

Please sign in to comment.