Commit 5361a64b authored by Ad Schellevis's avatar Ad Schellevis

(mvc, add constraint pattern) for https://github.com/opnsense/core/issues/272

Constraints hook into the default validations, new constraints should derive from BaseConstraint.
This commit contains the code needed to add constraints to our model and a unittest for the UniqueConstraint option.

Add the following to a model field, to force uniqueness for a combination of keys:

<Constraints>
  <check001>                                                 <!--validation name, unique within this field-->
    <message>number should be unique</message>               <!--the message to output on validation failure -->
    <type>OPNsense\Base\Constraints\UniqueConstraint</type>  <!--the class to construct, derived from OPNsense\Base\Constraints\BaseConstraint -->
    <addFields>                                              <!--optional additional fields for the equation, defined in UniqueConstraint -->
       <field1>optfield</field1>
    </addFields>
  </check001>
</Constraints>
parent b3f3a68b
<?php
/**
* Copyright (C) 2016 Deciso B.V.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
namespace OPNsense\Base\Constraints;
use \Phalcon\Validation\Validator;
use \Phalcon\Validation\ValidatorInterface;
use \Phalcon\Validation\Message;
abstract class BaseConstraint extends Validator implements ValidatorInterface
{
/**
* @param \Phalcon\Validation $validator
* @param $attribute
*/
protected function appendMessage(\Phalcon\Validation $validator, $attribute)
{
$message = $this->getOption('message');
$name = $this->getOption('name');
if (empty($message)) {
$message = 'validation failure ' . get_class($this);
}
if (empty($name)) {
$name = get_class($this);
}
$validator->appendMessage(new Message($message, $attribute, $name));
}
}
\ No newline at end of file
<?php
/**
* Copyright (C) 2016 Deciso B.V.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
*/
namespace OPNsense\Base\Constraints;
/**
* Class UniqueConstraint, add a unique constraint to this field and optional additional fields.
* @package OPNsense\Base\Constraints
*/
class UniqueConstraint extends BaseConstraint
{
/**
* Executes validation
*
* @param \Phalcon\Validation $validator
* @param string $attribute
* @return boolean
*/
public function validate(\Phalcon\Validation $validator, $attribute)
{
$node = $this->getOption('node');
$addFields = $this->getOption('addFields');
$fieldSeparator = chr(10) . chr(0);
if ($node) {
$containerNode = $node;
$nodeName = $node->getInternalXMLTagName();
$parentNode = $node->getParentNode();
$level = 0;
// dive into parent
while ($containerNode != null &&
get_class($containerNode) != 'OPNsense\Base\FieldTypes\ArrayField') {
$containerNode = $containerNode->getParentNode();
$level++;
}
if ($containerNode != null && $level == 2) {
// collect (additional) key fields
$keyFields = array($nodeName);
if (!empty($addFields)) {
foreach ($addFields as $field) {
$keyFields[] = $field;
}
}
// calculate the key for this node
$nodeKey = '';
foreach ($keyFields as $field) {
$nodeKey .= $fieldSeparator . $parentNode->$field;
}
// when an ArrayField is found in range, traverse nodes and compare keys
foreach ($containerNode->__items as $item) {
if ($item !== $parentNode) {
$itemKey = '';
foreach ($keyFields as $field) {
$itemKey .= $fieldSeparator . $item->$field;
}
if ($itemKey == $nodeKey) {
$this->appendMessage($validator, $attribute);
return false;
}
}
}
}
}
return true;
}
}
\ No newline at end of file
......@@ -47,6 +47,11 @@ abstract class BaseField
*/
protected $internalChildnodes = array();
/**
* @var array constraints for this field, additional to fieldtype
*/
protected $internalConstraints = array();
/**
* @var null pointer to parent
*/
......@@ -211,6 +216,15 @@ abstract class BaseField
$this->internalParentNode = $node;
}
/**
* return this nodes parent (or null if not found)
* @return null|BaseField
*/
public function getParentNode()
{
return $this->internalParentNode;
}
/**
* Reflect default getter to internal child nodes.
* Implements the special attribute __items to return all items and __reference to identify the field in this model.
......@@ -347,13 +361,35 @@ abstract class BaseField
}
}
/**
* fetch all additional validators
*/
private function getConstraintValidators()
{
$result = array();
foreach ($this->internalConstraints as $name => $constraint) {
if (!empty($constraint['type'])) {
try {
$constr_class = new \ReflectionClass($constraint['type']);
if ($constr_class->getParentClass()->name == 'OPNsense\Base\Constraints\BaseConstraint') {
$constraint['name'] = $name;
$constraint['node'] = $this;
$result[] = $constr_class->newInstance($constraint);
}
} catch (\ReflectionException $e) {
null; // ignore configuration errors, if the constraint can't be found, skip.
}
}
}
return $result;
}
/**
* return field validators for this field
* @return array returns validators for this field type (empty if none)
*/
public function getValidators()
{
$validators = array();
$validators = $this->getConstraintValidators();
if ($this->isEmptyAndRequired()) {
$validators[] = new PresenceOf(array('message' => $this->internalValidationMessage)) ;
}
......@@ -561,6 +597,15 @@ abstract class BaseField
}
}
/**
* set additional constraints
* @param $constraints
*/
public function setConstraints($constraints)
{
$this->internalConstraints = $constraints;
}
/**
* apply change case to this node, called by setValue
*/
......
......@@ -20,7 +20,18 @@
<MaximumValue>65535</MaximumValue>
<ValidationMessage>not a valid number</ValidationMessage>
<Required>Y</Required>
<Constraints>
<check001>
<message>number should be unique</message>
<type>OPNsense\Base\Constraints\UniqueConstraint</type>
<addFields>
<field1>optfield</field1>
</addFields>
</check001>
</Constraints>
</number>
<optfield type="TextField">
</optfield>
</item>
</arraytypes>
</items>
......
......@@ -234,4 +234,34 @@ class BaseModelTest extends \PHPUnit_Framework_TestCase
$data = BaseModelTest::$model->arraytypes->item->getNodes();
$this->assertEquals(count($data), 9);
}
/**
* @depends testGetNodes
* @expectedException \Phalcon\Validation\Exception
* @expectedExceptionMessage number should be unique
*/
public function testConstraintsNok()
{
$count = 2;
foreach (BaseModelTest::$model->arraytypes->item->__items as $nodeid => $node) {
$count-- ;
if ($count >= 0) {
$node->number = 999;
}
}
BaseModelTest::$model->serializeToConfig();
}
/**
* @depends testConstraintsNok
*/
public function testConstraintsOk()
{
$count = 1;
foreach (BaseModelTest::$model->arraytypes->item->__items as $nodeid => $node) {
$count++ ;
$node->number = $count;
}
BaseModelTest::$model->serializeToConfig();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment