Jump to content

Custom fieldtype save data problem


suntrop
 Share

Recommended Posts

I created a custom fieldtype to store two addresses. It receives its data from a custom form and will later use it in a PDF. However, the problem is, users can save billing and shipping address separately with two slightly different frontend forms. When I save one form only data saved by this from will be saved, all other fields get deleted.

image.thumb.png.d6829908fe3093503d311da265c115fd.png

So if the user saves his shipping address all billing fields are empty after the page is saved. First version of my fieldtype only had two textarea fields and everything worked fine. But now changed to have more control over every data field (name, street, etc.).

I started from copying the Dimension fieldtype, which was a copy of the MapMaker fieldtype I guess.

Problem should be inside Address::set() or the ___sleepValue()

// Frontend form to save billing address
$p->of(false);
$p->address->billing_gender     = sanitizer()->string( input()->post('gender') );
$p->address->billing_firstname  = sanitizer()->string( input()->post('first_name') );
$p->address->billing_lastname   = sanitizer()->string( input()->post('last_name') );
$p->address->billing_street     = sanitizer()->string( input()->post('street') );
$p->address->billing_zipcode    = sanitizer()->string( input()->post('zipcode') );
$p->address->billing_city       = sanitizer()->string( input()->post('city') );
$p->address->billing_company    = sanitizer()->string( input()->post('company') );
$p->address->billing_vatid      = sanitizer()->string( input()->post('vatid') );
$p->address->billing_country    = sanitizer()->int( input()->post('country') );
$save = $p->save('address');
<?php namespace ProcessWire;

/**
 * ProcessWire Address Fieldtype
 *
 */

class FieldtypeAddress extends Fieldtype {


	/**
	 * Initialize this Fieldtype
	 *
	 */
	public function init() {
		parent::init();
	}



	/**
	 * get Inputfield for this fieldtype, set config attributes so they can be used in the inputfield
	 *
	 */
	public function getInputfield(Page $page, Field $field) {
		$address = $this->modules->get('InputfieldAddress');
		return $address;
	}



	/**
	 * Get the database schema for this field
	 *
	 * @param Field $field In case it's needed for the schema, but usually should not.
	 * @return array
	 */
	public function getDatabaseSchema(Field $field) {

		$schema                            = parent::getDatabaseSchema($field);
		$schema['data']                    = 'TEXT NOT NULL'; // user last name
		$schema['data_billing_firstname']  = 'TEXT NOT NULL';
		$schema['data_billing_gender']     = 'TEXT NOT NULL';
		$schema['data_billing_company']    = 'TEXT NOT NULL';
		$schema['data_billing_vatid']      = 'TEXT NOT NULL';
		$schema['data_billing_street']     = 'TEXT NOT NULL';
		$schema['data_billing_zipcode']    = 'TEXT NOT NULL';
		$schema['data_billing_city']       = 'TEXT NOT NULL';
		$schema['data_billing_country']    = 'TEXT NOT NULL';
		$schema['data_shipping_lastname']  = 'TEXT NOT NULL';
		$schema['data_shipping_firstname'] = 'TEXT NOT NULL';
		$schema['data_shipping_company']   = 'TEXT NOT NULL';
		$schema['data_shipping_street']    = 'TEXT NOT NULL';
		$schema['data_shipping_zipcode']   = 'TEXT NOT NULL';
		$schema['data_shipping_city']      = 'TEXT NOT NULL';
		$schema['data_shipping_country']   = 'TEXT NOT NULL';
		$schema['data_shipping_department']= 'TEXT NOT NULL';
		$schema['data_shipping_gender']    = 'TEXT NOT NULL';

		// Remove index from data column
		unset($schema['keys']['data']);

		return $schema;
	}



	/**
	 * Any value will get sanitized before setting it to a page object
	 * and before saving the data
	 *
	 */
	public function sanitizeValue(Page $page, Field $field, $value) {

		if(!$value instanceof Address) $value = $this->getBlankValue($page, $field);

		if (
			$value->isChanged('billing_company') ||
			$value->isChanged('billing_gender') ||
			$value->isChanged('billing_vatid') ||
			$value->isChanged('billing_firstname') ||
			$value->isChanged('billing_lastname') ||
			$value->isChanged('billing_street') ||
			$value->isChanged('billing_zipcode') ||
			$value->isChanged('billing_city') ||
			$value->isChanged('billing_country') ||
			$value->isChanged('shipping_company') ||
			$value->isChanged('shipping_firstname') ||
			$value->isChanged('shipping_lastname') ||
			$value->isChanged('shipping_street') ||
			$value->isChanged('shipping_zipcode') ||
			$value->isChanged('shipping_city') ||
			$value->isChanged('shipping_country') ||
			$value->isChanged('shipping_department') ||
			$value->isChanged('shipping_gender')
		) {
			$page->trackChange($field->name);
		}

		return $value;
	}



	/**
	 * Get raw data values from database and perhaps adjust data for $page output
	 *
	 */
	public function ___wakeupValue(Page $page, Field $field, $value) {

		if(empty($value) ) return false;

		// get blank
		$address = $this->getBlankValue($page, $field); // TODO: required?

		$country = $this->pages()->get( (int)$value['data_billing_country'] );
		$address->billing = WireArray::new([
			'gender'     => (string)$value['data_billing_gender'],
			'company'    => (string)$value['data_billing_company'],
			'vatid'      => (string)$value['data_billing_vatid'],
			'first_name' => (string)$value['data_billing_firstname'],
			'last_name'  => (string)$value['data'],
			'street'     => (string)$value['data_billing_street'],
			'zipcode'    => (string)$value['data_billing_zipcode'],
			'city'       => (string)$value['data_billing_city'],
			'country'    => $country,
			'block'      => (string)$value['data_billing_company'] . PHP_EOL . (string)$value['data_billing_firstname'] . ' ' . (string)$value['data'] . PHP_EOL . (string)$value['data_billing_street'] . PHP_EOL . (string)$value['data_billing_zipcode'] . ' ' . (string)$value['data_billing_city'] . PHP_EOL . (string)$country->title,
		]);


		$country = $this->pages()->get( (int)$value['data_shipping_country'] );
		$address->shipping = WireArray::new([
			'gender'     => (string)$value['data_shipping_gender'],
			'company'    => (string)$value['data_shipping_company'],
			'department' => (string)$value['data_shipping_department'],
			'first_name' => (string)$value['data_shipping_firstname'],
			'last_name'  => (string)$value['data_shipping_lastname'],
			'street'     => (string)$value['data_shipping_street'],
			'zipcode'    => (string)$value['data_shipping_zipcode'],
			'city'       => (string)$value['data_shipping_city'],
			'country'    => $country,
			'block'      => (string)$value['data_shipping_company'] . PHP_EOL . (string)$value['data_shipping_firstname'] . ' ' . (string)$value['data_shipping_lastname'] . PHP_EOL . (string)$value['data_shipping_street'] . PHP_EOL . (string)$value['data_shipping_zipcode'] . ' ' . (string)$value['data_shipping_city'] . PHP_EOL . (string)$country->title,
		]);


		return $address;
	}



	/**
	 * Get data from $page and adjust data for storing in database
	 *
	 */
	public function ___sleepValue(Page $page, Field $field, $value) {

		// throw error if value is not of the right type
		if(!$value instanceof Address) throw new WireException('Expecting an instance of Address');

		$sleepValue = [
			'data_billing_company'     => (string)$value->billing_company,
			'data_billing_vatid'       => (string)$value->billing_vatid,
			'data_billing_gender'      => (string)$value->billing_gender,
			'data_billing_firstname'   => (string)$value->billing_firstname,
			'data'                     => (string)$value->billing_lastname,
			'data_billing_street'      => (string)$value->billing_street,
			'data_billing_zipcode'     => (string)$value->billing_zipcode,
			'data_billing_city'        => (string)$value->billing_city,
			'data_billing_country'     => (string)$value->billing_country,
			'data_shipping_company'    => (string)$value->shipping_company,
			'data_shipping_firstname'  => (string)$value->shipping_firstname,
			'data_shipping_lastname'   => (string)$value->shipping_lastname,
			'data_shipping_street'     => (string)$value->shipping_street,
			'data_shipping_zipcode'    => (string)$value->shipping_zipcode,
			'data_shipping_city'       => (string)$value->shipping_city,
			'data_shipping_country'    => (string)$value->shipping_country,
			'data_shipping_department' => (string)$value->shipping_department,
			'data_shipping_gender'     => (string)$value->shipping_gender,
		];


		return $sleepValue;
	}



	/**
	 * Format value for output
	 *
	 */
	public function ___formatValue(Page $page, Field $field, $value) {
		return $value;
	}



	/**
	 *
	 * Add mapping to different name for use in page selectors
	 * This enables us to use it like "field.min=10, field.max<=200, field.graduation>10"
	 */
	public function getMatchQuery($query, $table, $subfield, $operator, $value) {
		if ($subfield == 'billing_company') $subfield = 'data_billing_company';
		if ($subfield == 'billing_gender') $subfield = 'data_billing_gender';
		if ($subfield == 'billing_vatid') $subfield = 'data_billing_vatid';
		if ($subfield == 'billing_firstname') $subfield = 'data_billing_firstname';
		if ($subfield == 'billing_lastname') $subfield = 'data';
		if ($subfield == 'billing_street') $subfield = 'data_billing_street';
		if ($subfield == 'billing_zipcode') $subfield = 'data_billing_zipcode';
		if ($subfield == 'billing_city') $subfield = 'data_billing_city';
		if ($subfield == 'billing_country') $subfield = 'data_billing_country';
		if ($subfield == 'shipping_company') $subfield = 'data_shipping_company';
		if ($subfield == 'shipping_firstname') $subfield = 'data_shipping_firstname';
		if ($subfield == 'shipping_lastname') $subfield = 'data_shipping_lastname';
		if ($subfield == 'shipping_street') $subfield = 'data_shipping_street';
		if ($subfield == 'shipping_zipcode') $subfield = 'data_shipping_zipcode';
		if ($subfield == 'shipping_city') $subfield = 'data_shipping_city';
		if ($subfield == 'shipping_country') $subfield = 'data_shipping_country';
		if ($subfield == 'shipping_department') $subfield = 'data_shipping_department';
		if ($subfield == 'shipping_gender') $subfield = 'data_shipping_gender';

		return parent::getMatchQuery($query, $table, $subfield, $operator, $value);
	}



	/**
	 * there's none compatible
	 *
	 */
	public function ___getCompatibleFieldtypes(Field $field) {
		return null;
	}



	/**
	 * blank value is an WireData object Address
	 *
	 */
	public function getBlankValue(Page $page, Field $field) {
		return new Address();
	}



	/**
	 * Get any inputfields used for configuration of this Fieldtype.
	 *
	 * This is in addition any configuration fields supplied by the parent Inputfield.
	 *
	 * @param Field $field
	 * @return InputfieldWrapper
	 *
	 */
	public function ___getConfigInputfields(Field $field) {
		$inputfields = parent::___getConfigInputfields($field);
		// nothing yet
		return $inputfields;
	}

}





/**
 * Helper WireData Class to hold an address object
 *
 */
class Address extends WireData {

	public function __construct() {
		// bd('address construct');
		// $this->set('billing_company', null);
		// $this->set('billing_gender', null);
		// $this->set('billing_vatid', null);
		// $this->set('billing_firstname', null);
		// $this->set('billing_lastname', null);
		// $this->set('billing_street', null);
		// $this->set('billing_zipcode', null);
		// $this->set('billing_city', null);
		// $this->set('billing_country', null);
		// $this->set('shipping_company', null);
		// $this->set('shipping_firstname', null);
		// $this->set('shipping_lastname', null);
		// $this->set('shipping_street', null);
		// $this->set('shipping_zipcode', null);
		// $this->set('shipping_city', null);
		// $this->set('shipping_country', null);
		// $this->set('shipping_department', null);
		// $this->set('shipping_gender', null);
	}


	public function set($key, $value) {
		bd('VALUE: '.$value, 'address set: ' . $key );

		// if (
		// 	$key == 'billing_company' ||
		// 	$key == 'billing_gender' ||
		// 	$key == 'billing_vatid' ||
		// 	$key == 'billing_firstname' ||
		// 	$key == 'billing_lastname' ||
		// 	$key == 'billing_street' ||
		// 	$key == 'billing_zipcode' ||
		// 	$key == 'billing_city' ||
		// 	$key == 'billing_country' ||
		// 	$key == 'shipping_company' ||
		// 	$key == 'shipping_firstname' ||
		// 	$key == 'shipping_lastname' ||
		// 	$key == 'shipping_street' ||
		// 	$key == 'shipping_zipcode' ||
		// 	$key == 'shipping_city' ||
		// 	$key == 'shipping_department' ||
		// 	$key == 'shipping_gender' ||
		// 	$key == 'shipping_country'
		// ) {
		// 	if(!is_string($value) && !is_int($value) && !is_null($value)) {
		// 		$value = $this->$key ? $this->$key : 0;
		// 		throw new WireException("Address Object only accepts string (string) values");
		// 	}
		// }
		return parent::set($key, $value);
	}

	public function get($key) {
		// bd('address get: ' . $key);
		return parent::get($key);
	}

}

 

Link to comment
Share on other sites

I'd save keystrokes and struggles by unifying billing and shipping addresses into one single address fieldtype (you don't have to use the vat column if you are processing a shipping address, or the department if it's the other). Then you can add separate "shipping" and "billing" fields of the same type to your page and don't risk clearing one of those if you process the form for the other.

I've taken the liberty to amend the four most verbose methods using that approach:

<?php

	/**
	 * Get the database schema for this field
	 *
	 * @param Field $field In case it's needed for the schema, but usually should not.
	 * @return array
	 */
	public function getDatabaseSchema(Field $field) {

		$schema                     = parent::getDatabaseSchema($field);
		$schema['data']             = 'TEXT NOT NULL'; // user last name
		$schema['data_firstname']  	= 'TEXT NOT NULL';
		$schema['data_gender']     	= 'TEXT NOT NULL';
		$schema['data_company']    	= 'TEXT NOT NULL';
		$schema['data_vatid']      	= 'TEXT NOT NULL';
		$schema['data_street']     	= 'TEXT NOT NULL';
		$schema['data_zipcode']    	= 'TEXT NOT NULL';
		$schema['data_city']       	= 'TEXT NOT NULL';
		$schema['data_country']    	= 'INT(10) UNSIGNED';
		$schema['data_department'] 	= 'TEXT NOT NULL';

		// Remove index from data column
		unset($schema['keys']['data']);

		return $schema;
	}

	
	/**
	 * Any value will get sanitized before setting it to a page object
	 * and before saving the data
	 *
	 */
	public function sanitizeValue(Page $page, Field $field, $value) {

		if(!$value instanceof Address) $value = $this->getBlankValue($page, $field);

		if (
			$value->isChanged('company') ||
			$value->isChanged('gender') ||
			$value->isChanged('vatid') ||
			$value->isChanged('firstname') ||
			$value->isChanged('lastname') ||
			$value->isChanged('street') ||
			$value->isChanged('zipcode') ||
			$value->isChanged('city') ||
			$value->isChanged('department') ||
			$value->isChanged('country')
		) {
			$page->trackChange($field->name);
		}

		return $value;
	}


	/**
	 * Get raw data values from database and perhaps adjust data for $page output
	 *
	 */
	public function ___wakeupValue(Page $page, Field $field, $value) {

		if(empty($value) ) return false;

		// get blank
		$address = $this->getBlankValue($page, $field);

		$value['country'] = $this->pages()->get( (int)$value['data_billing_country'] );
		$value['block'] = (string)$value['data_company'] . PHP_EOL .
						  (string)$value['data_firstname'] . ' ' . (string)$value['data'] . PHP_EOL .
						  (string)$value['data_street'] . PHP_EOL . 
						  (string)$value['data_zipcode'] . ' ' . (string)$value['data_city'] . PHP_EOL .
						  (string)$country->title;
		
		$address->setArray($value);

		return $address;
	}

	/**
	 *
	 * Add mapping to different name for use in page selectors
	 * This enables us to use it like "field.min=10, field.max<=200, field.graduation>10"
	 */
	public function getMatchQuery($query, $table, $subfield, $operator, $value) {
		if ($subfield == 'city') $subfield = 'data_city';
		if ($subfield == 'company') $subfield = 'data_company';
		if ($subfield == 'country') $subfield = 'data_country';
		if ($subfield == 'department') $subfield = 'data_department';
		if ($subfield == 'firstname') $subfield = 'data_firstname';
		if ($subfield == 'gender') $subfield = 'data_gender';
		if ($subfield == 'lastname') $subfield = 'data';
		if ($subfield == 'street') $subfield = 'data_street';
		if ($subfield == 'vatid') $subfield = 'data_vatid';
		if ($subfield == 'zipcode') $subfield = 'data_zipcode';

		return parent::getMatchQuery($query, $table, $subfield, $operator, $value);
	}

 

Link to comment
Share on other sites

Thanks, but I don't get what you are trying to say. I need to have two separate addresses, because shipping is mostly other than billing address.

But I still can't see how PW works exactly saving data. First PW calls sanitizeValue() and next calls sleepValue(). But when I check $value in sleepValue()

$sleepValue = [
            'data_billing_company'     => isset($value->billing_company) ? $value->billing_company : $page->billing_company,
            //…
            'data_shipping_gender'     => isset($value->shipping_gender) ? $value->shipping_gender : $page->shipping_gender,
        ];

it doesn't work either. Seems to be all $value->xxx are always set, even when not set when saving data from API.

Link to comment
Share on other sites

1 hour ago, suntrop said:

I need to have two separate addresses

Yes. And since - unless I'm reading you wrong - you update those two addresses with two seperate forms, you should have two independent page fields, which will save you from all kinds of headaches. Since addresses are so similar, you can use the same field type with all necessary fields for each of the addresses and only include those fields in your form that apply to the address type. That way, you'll avoid all the headaches that come with updating just one of the addresses without overwriting the values of the other.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...