<?php

/*
 * This file is part of the symfony package.
 * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
 * (c) Jonathan H. Wage <jonwage@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

/**
 * sfValidatorDoctrineUnique validates that the uniqueness of a column.
 *
 * Warning: sfValidatorDoctrineUnique is susceptible to race conditions.
 * To avoid this issue, wrap the validation process and the model saving
 * inside a transaction.
 *
 * @package    symfony
 * @subpackage doctrine
 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
 * @author     Jonathan H. Wage <jonwage@gmail.com>
 * @version    SVN: $Id: sfValidatorDoctrineUnique.class.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
 */
class sfValidatorDoctrineUnique extends sfValidatorSchema
{
    /**
     * Constructor.
     *
     * @param array  An array of options
     * @param array  An array of error messages
     *
     * @see sfValidatorSchema
     */
    public function __construct($options = array(), $messages = array())
    {
        parent::__construct(null, $options, $messages);
    }

    /**
     * Configures the current validator.
     *
     * Available options:
     *
     *  * model:              The model class (required)
     *  * column:             The unique column name in Doctrine field name format (required)
     *                        If the uniquess is for several columns, you can pass an array of field names
     *  * primary_key:        The primary key column name in Doctrine field name format (optional, will be introspected if not provided)
     *                        You can also pass an array if the table has several primary keys
     *  * connection:         The Doctrine connection to use (null by default)
     *  * throw_global_error: Whether to throw a global error (false by default) or an error tied to the first field related to the column option array
     *
     * @see sfValidatorBase
     */
    protected function configure($options = array(), $messages = array())
    {
        $this->addOption('query', null);
        $this->addRequiredOption('model');
        $this->addRequiredOption('column');
        $this->addOption('primary_key', null);
        $this->addOption('connection', null);
        $this->addOption('skip_empty', null);
        $this->addOption('throw_global_error', false);

        $this->setMessage('invalid', 'Введенное значение занято');
    }

    /**
     * @see sfValidatorBase
     */
    protected function doClean($values)
    {
        $originalValues = $values;
        $table = Doctrine_Core::getTable($this->getOption('model'));
        if (!is_array($this->getOption('column'))) {
            $this->setOption('column', array($this->getOption('column')));
        }

        //if $values isn't an array, make it one
        if (!is_array($values)) {
            //use first column for key
            $columns = $this->getOption('column');
            $values = array($columns[0] => $values);
        }

        if ($q = $this->getOption('query')) {
            $q = clone $q;
        } else {
            $q = Doctrine_Core::getTable($this->getOption('model'))->createQuery();
        }

        foreach ($this->getOption('column') as $column) {
            $colName = $table->getColumnName($column);
            if (!array_key_exists($column, $values)) {
                // one of the column has be removed from the form
                return $originalValues;
            }

            $q->addWhere($colName . ' = ?', $values[$column]);
        }

        $object = $q->fetchOne();

        // if no object or if we're updating the object, it's ok
        if (!$object || $this->isUpdate($object, $values)) {
            return $originalValues;
        }

        $error = new sfValidatorError($this, 'invalid', array('column' => implode(', ', $this->getOption('column'))));

        if ($this->getOption('throw_global_error')) {
            throw $error;
        }

        $columns = $this->getOption('column');

        throw new sfValidatorErrorSchema($this, array($columns[0] => $error));
    }

    /**
     * Returns whether the object is being updated.
     *
     * @param BaseObject  A Doctrine object
     * @param array       An array of values
     *
     * @param Boolean     true if the object is being updated, false otherwise
     */
    protected function isUpdate(Doctrine_Record $object, $values)
    {
        // check each primary key column

        if ($this->getOption('skip_empty') === true) {
            foreach ($this->getOption('column') as $c) {
                if ($values[$c] === '' || $values[$c] === null) {
                    return true;
                }
            }
        }


        foreach ($this->getPrimaryKeys() as $column) {
            if (!isset($values[$column]) || $object->$column != $values[$column]) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns the primary keys for the model.
     *
     * @return array An array of primary keys
     */
    protected function getPrimaryKeys()
    {
        if (null === $this->getOption('primary_key')) {
            $primaryKeys = Doctrine_Core::getTable($this->getOption('model'))->getIdentifier();
            $this->setOption('primary_key', $primaryKeys);
        }

        if (!is_array($this->getOption('primary_key'))) {
            $this->setOption('primary_key', array($this->getOption('primary_key')));
        }

        return $this->getOption('primary_key');
    }
}