0

I have a complex nested (order) Zend\Form, that can be edited multiple times. The user first creates an order, but doesn't need to place it immediately. He can just save the order (or more exact: its data) and edit it later. In this case the application loads an Order object (with all its nested structure) and binds it to the form. The important steps are:

  1. get ID of the order from the request
  2. get the Order object by ID
  3. $orderForm->bind($orderObject) ...

Now I want to catch the data and serialize it to JSON. (The background: Forms cloning -- in the next step a empty new form should created and the should be passed to it; after saving we'll get a clone.) It should happen between 2 and 3. So I'm trying

 $formData = $this->orderForm->getData();
 $formJson = json_encode($formData, JSON_UNESCAPED_SLASHES);

and getting the error:

Zend\Form\Form::getData cannot return data as validation has not yet occurred

Well, I could try to work around it and validate the form:

$formIsValid = $this->orderForm->isValid();

but it only leads to further troubles:

Zend\InputFilter\BaseInputFilter::setData expects an array or Traversable argument; received NULL

Is there a way to get the form data before the validation?

rene
  • 41,474
  • 78
  • 114
  • 152
automatix
  • 14,018
  • 26
  • 105
  • 230
  • The last error message says, that the data you want to set with the setData method is null. You have to feed your form with data before validating it. Let 's refactor your way. First: Get the ID fo the order. Second: Get the order object by the ID. Third: Extract the data from your order object with an hydrator. Fourth: Give the extracted data (array) via the setData method of your form to the form. Fifth: validate it. Sixth: get the filtered, validated data of your form via getData method. You cannot bind the order object directly to your form. – Marcel Dec 19 '17 at 14:30

1 Answers1

1

Okay, the comment space is way too small to say everything about what you try to archive. Let 's refactor every single step you mentioned in the starting post. This will lead us to your goal. It 's all about hydration.

This will be a small example, how an order entity with products in it could look like. After the order entity follows the product entity, which we need for this example.

namespace Application\Entity;

class Order implements \JsonSerializable 
{
    /**
     * ID of the order
     * @var integer
     */
    protected $orderID;

    /**
     * Array of \Application\Entity\Product         
     * @var array 
     */
    protected $products;

    public function getOrderID() : integer
    {
        return $this->orderID;
    }

    public function setOrderID(integer $orderID) : Order
    {
        $this->orderID = $orderID;
        return $this;
    }

    public function getProducts()
    {
        if ($this->products == null) {
            $this->products = [];
        }

        return $this->products;
    }

    public function setProducts(array $products) : Order
    {
        $this->products = $products;
        return $this;
    }

    /**
     * @see \JsonSerializable::jsonSerialize()
     */
    public function jsonSerialize()
    {
        return get_object_vars($this);
    }
}

The following entity represents a product.

class Product implements \JsonSerializable
{
    protected $productID;

    protected $name;

    public function getProductID() : integer
    {
        return $this->productID;
    }

    public function setProductID(integer $productID) : Product
    {
        $this->productID = $productID;
        return $this;
    }

    public function getName() : string
    {
        return $this->name;
    }

    public function setName(string $name) : Product
    {
        $this->name = $name;
        return $this;
    }

    /**
     * @see \JsonSerializable::jsonSerialize()
     */
    public function jsonSerialize()
    {
        return get_object_vars($this);
    }
}

Above you see our entity, wich represents a single order with several possible products in it. The second member products can be an array with Product entities. This entity represents the data structure of our simple order.

At this point we need a form, which uses this entites as objects for the data it contains. A possible factory for our form could look like this.

namespace Application\Form\Factory;

class OrderFormFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $parentLocator = $serviceLocator->getServiceLocator();
        $inputFilter = $parentLocator->get('InputFilterManager')->get(OrderInputFiler::class);
        $hydrator = new ClassMethods(false);
        $entity = new OrderEntity();

        return (new OrderForm())
            ->setInputFilter($inputFilter)
            ->setHydrator($hydrator)
            ->setObject($entity);
    }
}

This is the factory for our form. We set a hydrator, an input filter and an entity for the form. So you don 't have to bind something. The following code shows, how to handle data with this form.

// retrieve an order from database by id
// This returns a order entity as result
$order = $this->getServiceLocator()->get(OrderTableGateway::class)->fetchById($id);

// Extract the order data from object to array assumed the
// retrieved data from data base is an OrderEntity object
// the hydrator will use the get* methods of the entity and returns an array
$data = (new ClassMethods(false))->extract($order);

// fill the form with the extracted data
$form = $this->getServiceLocator()->get('FormElementManager')->get(OrderForm::class);
$form->setData($data);

if ($form->isValid()) {

    // returns a validated order entity
    $order = $form->getData();
}

It is absolutely not possible to get data from a form, that is not validated yet. You have to validate the form data and after that you can get the filtered / validated data from the form. Hydrators and entities will help you a lot when you have to handle a lot of data.

Marcel
  • 4,854
  • 1
  • 14
  • 24