Prestashop 1.4 l’override, comment ça marche et ça sert à quoi ?

Si vous faites du développement sous Prestashop vous serez amenés tôt ou tard à utiliser l’override, qui existe depuis la version 1.4 de Prestashop. Il faut l’avoir fait une fois pour comprendre le principe, l’idée est donc de vous expliquer à quoi ça sert et comment le mettre en oeuvre avec un exemple simple.

C’est quoi l’override ?
L’override, c’est le principe de redéfinir un comportement de votre application, tout en conservant sa fonctionnalité « d’origine ». Sous Prestashop on peut par exemple overrider des « Classes » ou des « Controllers ».

A quoi ça sert, le but ?

En utilisant l’override on isole les fichiers modifiés dans un nouveau répertoire, cela permet de connaître les fichiers dont la source a été modifiée et cela permet de conserver les fichier originaux pour le fonctionnement par défaut. Cela facilite aussi les mises à jour, car vous pouvez remplacer les fichiers du coeur de Prestashop , sans vous préocuper de savoir si vous les aviez modifiés ou non (vos modifications sont dans un répertoire à part).

Quand est-ce que je dois utiliser l’override ?
Vous devez l’utiliser lorsque vous désirez ajouter / modifier un comportement de Prestashop, qui ne concerne « pas » les modules. Par exemple,  imaginons que votre client vous demande « J’aimerais que quand un membre s’inscrive sur le shop, je reçoive un email de notification. » Le problème c’est que Prestashop ne propose pas cette fonctionnalité, (ni dans son module d’alertes). Il faut donc modifier le comportement de l’application Prestashop.

déconseillé : la méthode sans l’override
L’idée serait de modifier directement le fichier du coeur, c’est-à-dire « controllers/AuthController.php ».

conseillé : la méthode avec l’override
L’idée serait de créer un nouveau fichier « clone » sous « override/controllers/AuthController.php » afin de conserver le fichier du coeur tel qu’il est défini par défaut.


Réalisons le cas pratique (demandé par le client ci-dessus)

1. Localiser où je dois faire la modification
Pas de miracle, il faut un peu fouiller dans le code, dans notre cas on veut envoyer le mail au marchand juste après que le client soit enregistré. Je trouve « $customer->add() » dans mon fichier « AuthController.php », c’est donc après cette ligne que je veux faire mon envoi d’email.

2. Créer un nouveau  Controller
Effectivement je vais devoir créer un nouveau controller, afin de conserver le controller l’original. Je peux créer un nouveau fichier sous « /override/controllers/AuthController.php ».
La nomination de la classe sera « AuthController » et devra étendre « AuthControllerCore », c’est-à-dire le controller que vous avez ouvert tout à l’heure sous /controllers/.

class AuthController extends AuthControllerCore {

Il faut prendre uniquement les méthodes que nous avons besoin, dans notre cas l’ajout du membre s’effectue via la méthode « preProcess() » je peux donc la copier complètement.
Dans ce nouveau controller n’ajoutez pas le constructeur, ni l’appel parent.

class AuthController extends AuthControllerCore
/*{
//public function __construct()
{
$this->ssl = true;
$this->php_self = 'authentication.php';</span>

parent::__construct();
}*/
public function preProcess()
{
//parent::preProcess();

Le code que nous allons ajouter pour envoyer le mail sera le suivant :

$msg = utf8_decode('Création d\'un nouveau compte pour '.$customer->firstname.' '.$customer->lastname);
$email = Configuration::get('PS_SHOP_EMAIL');
mail($email,$msg,$msg);

Voilà le fichier final « override/controllers/AuthController.php » avec l’injection de notre code « maison » à l’intérieur.

<?php
class AuthController extends AuthControllerCore
{

	public function preProcess()
	{

		if (self::$cookie->isLogged() AND !Tools::isSubmit('ajax'))
			Tools::redirect('my-account.php');

		if (Tools::getValue('create_account'))
		{
			$create_account = 1;
			self::$smarty->assign('email_create', 1);
		}

		if (Tools::isSubmit('SubmitCreate'))
		{
			if (!Validate::isEmail($email = Tools::getValue('email_create')) OR empty($email))
				$this->errors[] = Tools::displayError('Invalid e-mail address');
			elseif (Customer::customerExists($email))
			{
				$this->errors[] = Tools::displayError('An account is already registered with this e-mail, please fill in the password or request a new one.');
				$_POST['email'] = $_POST['email_create'];
				unset($_POST['email_create']);
			}
			else
			{
				$create_account = 1;
				self::$smarty->assign('email_create', Tools::safeOutput($email));
				$_POST['email'] = $email;
			}
		}

		if (Tools::isSubmit('submitAccount') OR Tools::isSubmit('submitGuestAccount'))
		{
			$create_account = 1;
			if (Tools::isSubmit('submitAccount'))
				self::$smarty->assign('email_create', 1);
			/* New Guest customer */
			if (!Tools::getValue('is_new_customer') AND !Configuration::get('PS_GUEST_CHECKOUT_ENABLED'))
				$this->errors[] = Tools::displayError('You cannot create a guest account.');
			if (!Tools::getValue('is_new_customer'))
				$_POST['passwd'] = md5(time()._COOKIE_KEY_);
			if (isset($_POST['guest_email']) AND $_POST['guest_email'])
				$_POST['email'] = $_POST['guest_email'];

			/* Preparing customer */
			$customer = new Customer();
			$lastnameAddress = $_POST['lastname'];
			$firstnameAddress = $_POST['firstname'];
			$_POST['lastname'] = $_POST['customer_lastname'];
			$_POST['firstname'] = $_POST['customer_firstname'];
			if (!Tools::getValue('phone') AND !Tools::getValue('phone_mobile'))
				$this->errors[] = Tools::displayError('You must register at least one phone number');
			$this->errors = array_unique(array_merge($this->errors, $customer->validateControler()));
			/* Preparing address */
			$address = new Address();
			$_POST['lastname'] = $lastnameAddress;
			$_POST['firstname'] = $firstnameAddress;
			$address->id_customer = 1;
			$this->errors = array_unique(array_merge($this->errors, $address->validateControler()));

			/* US customer: normalize the address */
			if($address->id_country == Country::getByIso('US'))
			{
				include_once(_PS_TAASC_PATH_.'AddressStandardizationSolution.php');
				$normalize = new AddressStandardizationSolution;
				$address->address1 = $normalize->AddressLineStandardization($address->address1);
				$address->address2 = $normalize->AddressLineStandardization($address->address2);
			}

			$zip_code_format = Country::getZipCodeFormat((int)(Tools::getValue('id_country')));
			if (Country::getNeedZipCode((int)(Tools::getValue('id_country'))))
			{
				if (($postcode = Tools::getValue('postcode')) AND $zip_code_format)
				{
					$zip_regexp = '/^'.$zip_code_format.'$/ui';
					$zip_regexp = str_replace(' ', '( |)', $zip_regexp);
					$zip_regexp = str_replace('-', '(-|)', $zip_regexp);
					$zip_regexp = str_replace('N', '[0-9]', $zip_regexp);
					$zip_regexp = str_replace('L', '[a-zA-Z]', $zip_regexp);
					$zip_regexp = str_replace('C', Country::getIsoById((int)(Tools::getValue('id_country'))), $zip_regexp);
					if (!preg_match($zip_regexp, $postcode))
						$this->errors[] = '<strong>'.Tools::displayError('Zip/ Postal code').'</strong> '.Tools::displayError('is invalid.').'<br />'.Tools::displayError('Must be typed as follows:').' '.str_replace('C', Country::getIsoById((int)(Tools::getValue('id_country'))), str_replace('N', '0', str_replace('L', 'A', $zip_code_format)));
				}
				elseif ($zip_code_format)
					$this->errors[] = '<strong>'.Tools::displayError('Zip/ Postal code').'</strong> '.Tools::displayError('is required.');
				elseif ($postcode AND !preg_match('/^[0-9a-zA-Z -]{4,9}$/ui', $postcode))
					$this->errors[] = '<strong>'.Tools::displayError('Zip/ Postal code').'</strong> '.Tools::displayError('is invalid.');
			}
			if (Country::isNeedDniByCountryId($address->id_country) AND !Tools::getValue('dni') AND !Validate::isDniLite(Tools::getValue('dni')))
				$this->errors[] = Tools::displayError('Identification number is incorrect or has already been used.');
			elseif (!Country::isNeedDniByCountryId($address->id_country))
				$address->dni = NULL;
			if (!@checkdate(Tools::getValue('months'), Tools::getValue('days'), Tools::getValue('years')) AND !(Tools::getValue('months') == '' AND Tools::getValue('days') == '' AND Tools::getValue('years') == ''))
				$this->errors[] = Tools::displayError('Invalid date of birth');
			if (!sizeof($this->errors))
			{
				if (Customer::customerExists(Tools::getValue('email')))
					$this->errors[] = Tools::displayError('An account is already registered with this e-mail, please fill in the password or request a new one.');
				if (Tools::isSubmit('newsletter'))
				{
					$customer->ip_registration_newsletter = pSQL(Tools::getRemoteAddr());
					$customer->newsletter_date_add = pSQL(date('Y-m-d H:i:s'));
				}

				$customer->birthday = (empty($_POST['years']) ? '' : (int)($_POST['years']).'-'.(int)($_POST['months']).'-'.(int)($_POST['days']));

				if (!sizeof($this->errors))
				{
					if (!$country = new Country($address->id_country, Configuration::get('PS_LANG_DEFAULT')) OR !Validate::isLoadedObject($country))
						die(Tools::displayError());
					if ((int)($country->contains_states) AND !(int)($address->id_state))
						$this->errors[] = Tools::displayError('This country requires a state selection.');
					else
					{
						$customer->active = 1;
						/* New Guest customer */
						if (Tools::isSubmit('is_new_customer'))
							$customer->is_guest = !Tools::getValue('is_new_customer', 1);
						else
							$customer->is_guest = 0;
						if (!$customer->add())
							$this->errors[] = Tools::displayError('An error occurred while creating your account.');
						else
						{
							$address->id_customer = (int)($customer->id);
							if (!$address->add())
								$this->errors[] = Tools::displayError('An error occurred while creating your address.');
							else
							{
								if (!$customer->is_guest)
								{
                                                                        // notification marchand
                                                                        $msg = utf8_decode('Création d\'un nouveau compte pour '.$customer->firstname.' '.$customer->lastname);
                                                                        $email = Configuration::get('PS_SHOP_EMAIL');
                                                                        mail($email,$msg,$msg);

									if (!Mail::Send((int)(self::$cookie->id_lang), 'account', Mail::l('Welcome!'),
									array('{firstname}' => $customer->firstname, '{lastname}' => $customer->lastname, '{email}' => $customer->email, '{passwd}' => Tools::getValue('passwd')), $customer->email, $customer->firstname.' '.$customer->lastname))
										$this->errors[] = Tools::displayError('Cannot send email');

								}
								self::$smarty->assign('confirmation', 1);
								self::$cookie->id_customer = (int)($customer->id);
								self::$cookie->customer_lastname = $customer->lastname;
								self::$cookie->customer_firstname = $customer->firstname;
								self::$cookie->passwd = $customer->passwd;
								self::$cookie->logged = 1;
								self::$cookie->email = $customer->email;
								self::$cookie->is_guest = !Tools::getValue('is_new_customer', 1);
								/* Update cart address */
								self::$cart->secure_key = $customer->secure_key;
								self::$cart->id_address_delivery = Address::getFirstCustomerAddressId((int)($customer->id));
								self::$cart->id_address_invoice = Address::getFirstCustomerAddressId((int)($customer->id));
								self::$cart->update();
								Module::hookExec('createAccount', array(
									'_POST' => $_POST,
									'newCustomer' => $customer
								));
								if (Tools::isSubmit('ajax'))
								{
									$return = array(
										'hasError' => !empty($this->errors),
										'errors' => $this->errors,
										'isSaved' => true,
										'id_customer' => (int)self::$cookie->id_customer,
										'id_address_delivery' => self::$cart->id_address_delivery,
										'id_address_invoice' => self::$cart->id_address_invoice,
										'token' => Tools::getToken(false)
									);
									die(Tools::jsonEncode($return));
								}
								if ($back = Tools::getValue('back'))
									Tools::redirect($back);
								Tools::redirect('my-account.php');
							}
						}
					}
				}
			}
			if (sizeof($this->errors))
			{
				if (!Tools::getValue('is_new_customer'))
					unset($_POST['passwd']);
				if (Tools::isSubmit('ajax'))
				{
					$return = array(
						'hasError' => !empty($this->errors),
						'errors' => $this->errors,
						'isSaved' => false,
						'id_customer' => 0
					);
					die(Tools::jsonEncode($return));
				}
			}
		}

		if (Tools::isSubmit('SubmitLogin'))
		{
			Module::hookExec('beforeAuthentication');
			$passwd = trim(Tools::getValue('passwd'));
			$email = trim(Tools::getValue('email'));
			if (empty($email))
				$this->errors[] = Tools::displayError('E-mail address required');
			elseif (!Validate::isEmail($email))
				$this->errors[] = Tools::displayError('Invalid e-mail address');
			elseif (empty($passwd))
				$this->errors[] = Tools::displayError('Password is required');
			elseif (Tools::strlen($passwd) > 32)
				$this->errors[] = Tools::displayError('Password is too long');
			elseif (!Validate::isPasswd($passwd))
				$this->errors[] = Tools::displayError('Invalid password');
			else
			{
				$customer = new Customer();
				$authentication = $customer->getByEmail(trim($email), trim($passwd));
				if (!$authentication OR !$customer->id)
				{
					/* Handle brute force attacks */
					sleep(1);
					$this->errors[] = Tools::displayError('Authentication failed');
				}
				else
				{
					self::$cookie->id_customer = (int)($customer->id);
					self::$cookie->customer_lastname = $customer->lastname;
					self::$cookie->customer_firstname = $customer->firstname;
					self::$cookie->logged = 1;
					self::$cookie->is_guest = $customer->isGuest();
					self::$cookie->passwd = $customer->passwd;
					self::$cookie->email = $customer->email;
					if (Configuration::get('PS_CART_FOLLOWING') AND (empty(self::$cookie->id_cart) OR Cart::getNbProducts(self::$cookie->id_cart) == 0))
						self::$cookie->id_cart = (int)(Cart::lastNoneOrderedCart((int)($customer->id)));
					/* Update cart address */
					self::$cart->id_address_delivery = Address::getFirstCustomerAddressId((int)($customer->id));
					self::$cart->id_address_invoice = Address::getFirstCustomerAddressId((int)($customer->id));
					self::$cart->update();
					Module::hookExec('authentication');
					if (!Tools::isSubmit('ajax'))
					{
						if ($back = Tools::getValue('back'))
							Tools::redirect($back);
						Tools::redirect('my-account.php');
					}
				}
			}
			if (Tools::isSubmit('ajax'))
			{
				$return = array(
					'hasError' => !empty($this->errors),
					'errors' => $this->errors,
					'token' => Tools::getToken(false)
				);
				die(Tools::jsonEncode($return));
			}
		}

		if (isset($create_account))
		{
			/* Select the most appropriate country */
			if (isset($_POST['id_country']) AND is_numeric($_POST['id_country']))
				$selectedCountry = (int)($_POST['id_country']);
			/* FIXME : language iso and country iso are not similar,
			 * maybe an associative table with country an language can resolve it,
			 * But for now it's a bug !
			 * @see : bug #6968
			 * @link:http://www.prestashop.com/bug_tracker/view/6968/
			elseif (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
			{
				$array = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
				if (Validate::isLanguageIsoCode($array[0]))
				{
					$selectedCountry = Country::getByIso($array[0]);
					if (!$selectedCountry)
						$selectedCountry = (int)(Configuration::get('PS_COUNTRY_DEFAULT'));
				}
			}*/
			if (!isset($selectedCountry))
				$selectedCountry = (int)(Configuration::get('PS_COUNTRY_DEFAULT'));
			$countries = Country::getCountries((int)(self::$cookie->id_lang), true);

			self::$smarty->assign(array(
				'countries' => $countries,
				'sl_country' => (isset($selectedCountry) ? $selectedCountry : 0),
				'vat_management' => Configuration::get('VATNUMBER_MANAGEMENT')
			));

			/* Call a hook to display more information on form */
			self::$smarty->assign(array(
				'HOOK_CREATE_ACCOUNT_FORM' => Module::hookExec('createAccountForm'),
				'HOOK_CREATE_ACCOUNT_TOP' => Module::hookExec('createAccountTop')
			));
		}

		/* Generate years, months and days */
		if (isset($_POST['years']) AND is_numeric($_POST['years']))
			$selectedYears = (int)($_POST['years']);
		$years = Tools::dateYears();
		if (isset($_POST['months']) AND is_numeric($_POST['months']))
			$selectedMonths = (int)($_POST['months']);
		$months = Tools::dateMonths();

		if (isset($_POST['days']) AND is_numeric($_POST['days']))
			$selectedDays = (int)($_POST['days']);
		$days = Tools::dateDays();

		self::$smarty->assign(array(
			'years' => $years,
			'sl_year' => (isset($selectedYears) ? $selectedYears : 0),
			'months' => $months,
			'sl_month' => (isset($selectedMonths) ? $selectedMonths : 0),
			'days' => $days,
			'sl_day' => (isset($selectedDays) ? $selectedDays : 0)
		));
	}
}

Bilan
Et voilà, nous avons modifié le comportement par défaut de Prestashop, tout en conservant les fichiers originaux. Ce n’est pas très difficile dans notre cas, mais cela peut-être bien plus complexe suivant ce que vous devez modifier. En démontrant le concept, vous avez à présent la connaissance de base pour ajouter/modifier Prestashop dans les règles de l’art, afin d’améliorer grandement la maintenance de votre code.

Notez mon billet, Google va adorer :
1 étoiles - J'aime pas !2 étoiles - Bof !3 étoiles - Bien !4 étoiles - Très bien !5 étoiles - Génial ! (Soyez le premier à noter ce billet)
Loading...

13 commentaires sur “Prestashop 1.4 l’override, comment ça marche et ça sert à quoi ?”

  1. J’aime ! Effectivement, ce tout nouveau principe (bien connu dans certain logiciel) permet une simplification de la modification par le ciblage de celle-ci.

    Une belle réussite, bien que l’utilisation de « eval » à l’appel de chaque objet me dérange d’un point de vu strictement fuite mémoire.

    Merci pour l’article 😉

  2. Merci pour l’article.

    Le seul hic est que si nous sommes plusieurs à vouloir intervenir sur l’override d’une même classe, l’intervention ne peux se faire autrement que de modifier la classe override existante. Ce qui devient bloquant pour le principe modulaire du coeur.

    Cordialement

  3. Salut,

    Je suis embêtant mais ce n’est pas un contre exemple ? ou l’exemple de ce qui ne faudrait pas faire ?
    Il aurait été plus simple de faire un module qui se greffe au hook « createAccount » non ?

    Cf au début : « Vous devez l’utiliser lorsque vous désirez ajouter / modifier un comportement de Prestashop, qui ne concerne « pas » les modules. » Là c’est le rôle d’un module plutôt.

  4. Et quand tu met à jour prestashop, tu vérifie classe par classe pour savoir si l’équipe a changé une ligne quelque part ?

    Si oui, je préfère modifier le code source en laissant une marque dans un commentaire. Ensuite avec winmerge je vois direct si une ligne a été changé par l’équipe ou par moi.

    Et avec override tu peux donc rater des mises à jour critique en pensant être a jour!! Non??

    1. Hello, pour le moment je n’ai pas encore assez de recul pour me prononcer sur les mises à jour avec l’override, mais de toute façon cela ne change pas le fait qu’il faudra passer au travers du code pour l’ajuster (avec ou sans override). Par contre au moment du développement cela est bien pratique pour switcher sur le comportement normal, pour effectuer la comparaison ou pour vérifier si un problème est présent déjà dans le comportement par défaut.

  5. Merci pour ce bon article, je ne connaissais pas ce fonctionnement très astucieux et sécurisé. Je vais désormais beaucoup plus exploiter l’override dans mes devs Prestashop.
    A++

  6. Hi !
    Tout d’abord merci pour ce tuto et cette explication !
    Pour ma boutique les inscriptions ne sont pas activé tant que nous (administrateurs) ayons validé leur compte.
    J’aurai donc besoin de les renvoyer sur une page leur expliquant que leur demande de création de compte est bien enregistrée et qu’il recevrons un mail lorsque leur compte aura été approuvé par nos administrateurs, jute après la validation du formulaire d’inscription.
    Comment pourrai-je faire en utilisant l’override ?

    D’avance merci pour vos explications !

    1. Bonjour,
      Si vous validez depuis votre back-office le compte du client, il faudra effectuer une modification du code en dur dans les fichiers de l’admin « là où se produit l’approbation du compte » et ajouter le traitement de l’envoi d’email à cet emplacement. Pour la partie admin du site il n’existe pas encore la possibilité d’override.

  7. Bonjour,

    Je viens de suivre pas à pas ton exemple, et tout s’est bien déroulé, et surtout je pensais avoir tout compris, ERREUR ! ! !

    J’ai eu des soucis avec le fil d’Ariane, il était en double. Dans le forum PS j’ai eu la solution en modifiant le TOOLS.PHP, et on m’a conseillé de mettre les modification dans un override, et là patatra . . . tout les site en vrac.

    J’ai récupéré le tools.php dans classes, j’ai enlevé les modification pour le remettre d’origine et je l’ai enregistré. Là mon fil d’Ariane est bien revenu doublé.
    J’ai modifié le début de la class :
    class Tools extends ToolsCore
    J’ai modifié les deux lignes, et j’ai tout enregistré dans override/classes.
    résultat impossible d’accéder au site.
    Où ai-je merdé ?
    aurais-tu une idée ?

    Merci et bonne semaine

    PS 1.4.6.2

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *