source

PDO 준비 문에서 원시 SQL 쿼리 문자열을 가져오는 중

bestscript 2023. 1. 16. 20:08

PDO 준비 문에서 원시 SQL 쿼리 문자열을 가져오는 중

준비된 스테이트먼트에서 PDOState::execute()를 호출할 때 raw SQL 문자열을 실행하는 방법이 있습니까?디버깅을 위해 이것은 매우 유용합니다.

파라미터 값이 삽입된 최종 SQL 쿼리를 원한다는 말씀이신 것 같습니다.이것이 디버깅에 도움이 되는 것은 이해하지만 준비된 문장으로 동작하는 방식은 아닙니다.매개 변수는 클라이언트 측에서 준비된 문과 결합되지 않으므로 PDO는 매개 변수와 결합된 쿼리 문자열에 액세스할 수 없습니다.

SQL 문은 준비() 시 데이터베이스 서버로 전송되며 파라미터는 실행() 시 별도로 전송됩니다.MySQL의 일반 쿼리 로그에 값을 보간한 최종 SQL이 표시됩니다().다음은 저의 일반적인 질의 로그에서 발췌한 것입니다.PDO가 아닌 mysql CLI에서 쿼리를 실행했지만 원칙은 동일합니다.

081016 16:51:28 2 Query       prepare s1 from 'select * from foo where i = ?'
                2 Prepare     [2] select * from foo where i = ?
081016 16:51:39 2 Query       set @a =1
081016 16:51:47 2 Query       execute s1 using @a
                2 Execute     [2] select * from foo where i = 1

PDO Atribut PDO 를 설정하면, 다음과 같이 원하는 것을 얻을 수 있습니다.ATTR_EMULATE_PREPARES이 모드에서는 PDO가 파라미터를 SQL 쿼리에 보간하여 실행() 시 쿼리 전체를 전송합니다.이것은 진정한 준비된 쿼리가 아닙니다.execute() 전에 SQL 문자열에 변수를 삽입함으로써 준비된 쿼리의 이점을 회피할 수 있습니다.


@afilina로부터의 재코멘트:

아니요. 텍스트 SQL 쿼리는 실행 중 매개 변수와 결합되지 않습니다.PDO가 보여줄 수 있는 건 아무것도 없어요

내부적으로 PDO를 사용하는 경우:ATR_EMULATE_PREPARES, PDO는 준비 및 실행을 수행하기 전에 SQL 쿼리의 복사본을 만들고 여기에 매개 변수 값을 삽입합니다.그러나 PDO는 이 수정된 SQL 쿼리를 노출하지 않습니다.

PDOStatement 개체에 $queryString 속성이 있지만 이 속성은 PDOStatement의 생성자에서만 설정되며 쿼리를 매개 변수로 다시 작성할 때 업데이트되지 않습니다.

PDO가 다시 작성된 쿼리를 공개하도록 요청하는 것은 합리적인 기능 요청입니다.그러나 PDO를 사용하지 않는 한 "완전한" 쿼리를 얻을 수 없습니다.ATTR_EMULATE_PREPARES

이 경우 파라미터 자리 표시자가 있는 준비된 쿼리가 서버에서 다시 작성되고 파라미터 값이 쿼리 문자열에 다시 입력되기 때문에 MySQL 서버의 일반 쿼리 로그를 사용하는 위의 회피책을 보여 줍니다.그러나 이 작업은 로깅 중에만 수행되며 쿼리 실행 중에는 수행되지 않습니다.

/**
 * Replaces any parameter placeholders in a query with the value of that
 * parameter. Useful for debugging. Assumes anonymous parameters from 
 * $params are are in the same order as specified in $query
 *
 * @param string $query The sql query with parameter placeholders
 * @param array $params The array of substitution parameters
 * @return string The interpolated query
 */
public static function interpolateQuery($query, $params) {
    $keys = array();

    # build a regular expression for each parameter
    foreach ($params as $key => $value) {
        if (is_string($key)) {
            $keys[] = '/:'.$key.'/';
        } else {
            $keys[] = '/[?]/';
        }
    }

    $query = preg_replace($keys, $params, $query, 1, $count);

    #trigger_error('replaced '.$count.' keys');

    return $query;
}

WHERE IN(?)등의 스테이트먼트에 어레이의 출력을 처리하는 것을 포함하도록 메서드를 변경했습니다.

업데이트: NULL 값 체크가 추가되어 $param 값이 중복되어 실제 $param 값은 변경되지 않습니다.

웹 친구 수고하셨습니다.고마워요!

/**
 * Replaces any parameter placeholders in a query with the value of that
 * parameter. Useful for debugging. Assumes anonymous parameters from 
 * $params are are in the same order as specified in $query
 *
 * @param string $query The sql query with parameter placeholders
 * @param array $params The array of substitution parameters
 * @return string The interpolated query
 */
public function interpolateQuery($query, $params) {
    $keys = array();
    $values = $params;

    # build a regular expression for each parameter
    foreach ($params as $key => $value) {
        if (is_string($key)) {
            $keys[] = '/:'.$key.'/';
        } else {
            $keys[] = '/[?]/';
        }

        if (is_string($value))
            $values[$key] = "'" . $value . "'";

        if (is_array($value))
            $values[$key] = "'" . implode("','", $value) . "'";

        if (is_null($value))
            $values[$key] = 'NULL';
    }

    $query = preg_replace($keys, $values, $query);

    return $query;
}

해결책은 자발적으로 쿼리에 오류를 넣고 오류 메시지를 인쇄하는 것입니다.

//Connection to the database
$co = new PDO('mysql:dbname=myDB;host=localhost','root','');
//We allow to print the errors whenever there is one
$co->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

//We create our prepared statement
$stmt = $co->prepare("ELECT * FROM Person WHERE age=:age"); //I removed the 'S' of 'SELECT'
$stmt->bindValue(':age','18',PDO::PARAM_STR);
try {
    $stmt->execute();
} catch (PDOException $e) {
    echo $e->getMessage();
}

표준 출력:

SQLSTATE [ 42000 ]:구문 오류 또는 액세스 위반: 1행의 'ELECT * FROM WHERE age=18' 근처 [...]

중요한 것은 쿼리의 처음 80자만 출력된다는 것입니다.

늦었을지도 , 은 있다.PDOStatement::debugDumpParams

준비된 스테이트먼트에 포함된 정보를 출력에 직접 덤프합니다.사용 중인 SQL 쿼리, 사용된 파라미터 수(Params), 파라미터 목록, 이름, 유형(paramtype), 키 이름 또는 위치, 쿼리의 위치(PDO 드라이버에서 지원되는 경우 -1이 됩니다).

자세한 내용은 공식 php 문서를 참조하십시오.

예:

<?php
/* Execute a prepared statement by binding PHP variables */
$calories = 150;
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
    FROM fruit
    WHERE calories < :calories AND colour = :colour');
$sth->bindParam(':calories', $calories, PDO::PARAM_INT);
$sth->bindValue(':colour', $colour, PDO::PARAM_STR, 12);
$sth->execute();

$sth->debugDumpParams();

?>

Mike에 의해 코드에 조금 더 추가 - 작은 따옴표를 추가할 값을 입력합니다.

/**
 * Replaces any parameter placeholders in a query with the value of that
 * parameter. Useful for debugging. Assumes anonymous parameters from 
 * $params are are in the same order as specified in $query
 *
 * @param string $query The sql query with parameter placeholders
 * @param array $params The array of substitution parameters
 * @return string The interpolated query
 */
public function interpolateQuery($query, $params) {
    $keys = array();
    $values = $params;

    # build a regular expression for each parameter
    foreach ($params as $key => $value) {
        if (is_string($key)) {
            $keys[] = '/:'.$key.'/';
        } else {
            $keys[] = '/[?]/';
        }

        if (is_array($value))
            $values[$key] = implode(',', $value);

        if (is_null($value))
            $values[$key] = 'NULL';
    }
    // Walk the array to see if we can add single-quotes to strings
    array_walk($values, create_function('&$v, $k', 'if (!is_numeric($v) && $v!="NULL") $v = "\'".$v."\'";'));

    $query = preg_replace($keys, $values, $query, 1, $count);

    return $query;
}

PDOStatement에 공용 속성 $queryString이 있습니다.그게 네가 원하는 거야

PDOStatement에 문서화되어 있지 않은 메서드 debugDumpParams()가 있다는 것을 알게 되었습니다.이 메서드 debugDumpParams()도 참조해 주십시오.

PDOStatement 클래스를 확장하여 경계 변수를 캡처하고 나중에 사용할 수 있도록 저장할 수 있습니다.다음으로 변수 saniting(debugBindedVariables)과 이들 변수를 사용하여 쿼리를 인쇄하는 방법(debugQuery)의 2가지 방법을 추가할 수 있습니다.

class DebugPDOStatement extends \PDOStatement{
  private $bound_variables=array();
  protected $pdo;

  protected function __construct($pdo) {
    $this->pdo = $pdo;
  }

  public function bindValue($parameter, $value, $data_type=\PDO::PARAM_STR){
    $this->bound_variables[$parameter] = (object) array('type'=>$data_type, 'value'=>$value);
    return parent::bindValue($parameter, $value, $data_type);
  }

  public function bindParam($parameter, &$variable, $data_type=\PDO::PARAM_STR, $length=NULL , $driver_options=NULL){
    $this->bound_variables[$parameter] = (object) array('type'=>$data_type, 'value'=>&$variable);
    return parent::bindParam($parameter, $variable, $data_type, $length, $driver_options);
  }

  public function debugBindedVariables(){
    $vars=array();

    foreach($this->bound_variables as $key=>$val){
      $vars[$key] = $val->value;

      if($vars[$key]===NULL)
        continue;

      switch($val->type){
        case \PDO::PARAM_STR: $type = 'string'; break;
        case \PDO::PARAM_BOOL: $type = 'boolean'; break;
        case \PDO::PARAM_INT: $type = 'integer'; break;
        case \PDO::PARAM_NULL: $type = 'null'; break;
        default: $type = FALSE;
      }

      if($type !== FALSE)
        settype($vars[$key], $type);
    }

    if(is_numeric(key($vars)))
      ksort($vars);

    return $vars;
  }

  public function debugQuery(){
    $queryString = $this->queryString;

    $vars=$this->debugBindedVariables();
    $params_are_numeric=is_numeric(key($vars));

    foreach($vars as $key=>&$var){
      switch(gettype($var)){
        case 'string': $var = "'{$var}'"; break;
        case 'integer': $var = "{$var}"; break;
        case 'boolean': $var = $var ? 'TRUE' : 'FALSE'; break;
        case 'NULL': $var = 'NULL';
        default:
      }
    }

    if($params_are_numeric){
      $queryString = preg_replace_callback( '/\?/', function($match) use( &$vars) { return array_shift($vars); }, $queryString);
    }else{
      $queryString = strtr($queryString, $vars);
    }

    echo $queryString.PHP_EOL;
  }
}


class DebugPDO extends \PDO{
  public function __construct($dsn, $username="", $password="", $driver_options=array()) {
    $driver_options[\PDO::ATTR_STATEMENT_CLASS] = array('DebugPDOStatement', array($this));
    $driver_options[\PDO::ATTR_PERSISTENT] = FALSE;
    parent::__construct($dsn,$username,$password, $driver_options);
  }
}

그런 다음 이 상속된 클래스를 사용하여 목적의 디버깅을 수행할 수 있습니다.

$dbh = new DebugPDO('mysql:host=localhost;dbname=test;','user','pass');

$var='user_test';
$sql=$dbh->prepare("SELECT user FROM users WHERE user = :test");
$sql->bindValue(':test', $var, PDO::PARAM_STR);
$sql->execute();

$sql->debugQuery();
print_r($sql->debugBindedVariables());

그 결과

사용자에서 사용자 선택 WHERE 사용자 = 'user_test'

어레이 ( [:test] => user_test )

나는 내 필요에 따라 이 상황을 조사하는데 많은 시간을 보냈다.이것과 다른 몇 가지 SO 스레드가 큰 도움이 되었기 때문에 제가 생각해낸 것을 공유하고 싶었습니다.

트러블 슈팅 중에는 보간된 쿼리 문자열에 액세스할 수 있는 것이 큰 장점이지만 특정 쿼리의 로그만 유지할 수 있기를 원했습니다(따라서 데이터베이스 로그를 이 용도로 사용하는 것은 이상적이지 않습니다).또한 로그를 사용하여 언제든지 테이블 상태를 재현할 수 있기를 원했기 때문에 보간 문자열이 올바르게 이스케이프되었는지 확인해야 했습니다.마지막으로 이 기능을 코드 베이스 전체로 확장하여 가능한 한 적은 수의 코드 베이스(데드라인, 마케팅 등)를 다시 작성해야 했습니다.

이 솔루션은 기본 PDOStatement 객체의 기능을 확장하여 파라미터화된 값(또는 참조)을 캐시하고 문을 실행할 때 PDO 객체의 기능을 사용하여 파라미터가 쿼리 문자열에 다시 삽입될 때 파라미터를 적절히 이스케이프하는 것이었습니다.그런 다음 문 객체의 메서드를 실행하고 그 때 실행된 실제 쿼리를 기록할 수 있습니다(또는 가능한 복제에 충실합니다).

설명한 바와 같이 이 베이스 싶지 에 기본 .bindParam() ★★★★★★★★★★★★★★★★★」bindValue()객체의 하고 PDOStatement를 호출합니다.parent::bindParam()는는: :bindValue()이것에 의해, 기존의 코드 베이스는 정상적으로 기능할 수 있게 되었습니다.

「」가 때, 「」가execute() 문자열을 새 합니다.E_PDOStatement->fullQuery쿼리를 표시하기 위해 출력하거나 로그 파일에 쓸 수 있습니다.

확장 기능은 설치 및 구성 절차와 함께 github에서 사용할 수 있습니다.

https://github.com/noahheck/E_PDOStatement

면책사항:
분명히, 내가 언급했듯이, 이 확장자는 내가 쓴 것이다.여기에서는 여러 스레드의 도움을 받아 개발되었기 때문에 저와 마찬가지로 다른 사람이 이러한 스레드를 접하게 될 경우를 대비해 여기에 솔루션을 게시하고 싶었습니다.

하시면 됩니다.sprintf(str_replace('?', '"%s"', $sql), ...$params);

다음은 예를 제시하겠습니다.

function mysqli_prepared_query($link, $sql, $types='', $params=array()) {
    echo sprintf(str_replace('?', '"%s"', $sql), ...$params);
    //prepare, bind, execute
}

$link = new mysqli($server, $dbusername, $dbpassword, $database);
$sql = "SELECT firstname, lastname FROM users WHERE userage >= ? AND favecolor = ?";
$types = "is"; //integer and string
$params = array(20, "Brown");

if(!$qry = mysqli_prepared_query($link, $sql, $types, $params)){
    echo "Failed";
} else {
    echo "Success";
}

이 기능은 PHP > = 5.6에서만 작동합니다.

기존의 답변 중 어느 것도 완전하거나 안전한 것 같지 않아 이 기능을 생각해 냈습니다.이 기능은 다음과 같이 개선되었습니다.

  • 는, 이름 없는 양쪽 모두와 함께 동작합니다).?( ) 및 이름( ):foo) 파라미터입니다.

  • PDO:: quote()를 사용하여 값이 아닌 값을 올바르게 이스케이프합니다.NULL,int,float또는bool.

  • 다음을 포함하는 문자열 값을 적절하게 처리합니다."?"그리고.":foo"플레이스홀더로 착각하지 않고요

    function interpolateSQL(PDO $pdo, string $query, array $params) : string {
        $s = chr(2); // Escape sequence for start of placeholder
        $e = chr(3); // Escape sequence for end of placeholder
        $keys = [];
        $values = [];

        // Make sure we use escape sequences that are not present in any value
        // to escape the placeholders.
        foreach ($params as $key => $value) {
            while( mb_stripos($value, $s) !== false ) $s .= $s;
            while( mb_stripos($value, $e) !== false ) $e .= $e;
        }
        
        
        foreach ($params as $key => $value) {
            // Build a regular expression for each parameter
            $keys[] = is_string($key) ? "/$s:$key$e/" : "/$s\?$e/";

            // Treat each value depending on what type it is. 
            // While PDO::quote() has a second parameter for type hinting, 
            // it doesn't seem reliable (at least for the SQLite driver).
            if( is_null($value) ){
                $values[$key] = 'NULL';
            }
            elseif( is_int($value) || is_float($value) ){
                $values[$key] = $value;
            }
            elseif( is_bool($value) ){
                $values[$key] = $value ? 'true' : 'false';
            }
            else{
                $value = str_replace('\\', '\\\\', $value);
                $values[$key] = $pdo->quote($value);
            }
        }

        // Surround placehodlers with escape sequence, so we don't accidentally match
        // "?" or ":foo" inside any of the values.
        $query = preg_replace(['/\?/', '/(:[a-zA-Z0-9_]+)/'], ["$s?$e", "$s$1$e"], $query);

        // Replace placeholders with actual values
        $query = preg_replace($keys, $values, $query, 1, $count);

        // Verify that we replaced exactly as many placeholders as there are keys and values
        if( $count !== count($keys) || $count !== count($values) ){
            throw new \Exception('Number of replacements not same as number of keys and/or values');
        }

        return $query;
    }

나는 그것이 더 개선될 것이라고 확신한다.

저 같은 경우에는 결국 JSON 인코딩 매개 변수와 함께 실제 "준비되지 않은 쿼리"(플레이스 홀더를 포함하는 SQL)만 기록하게 되었습니다.그러나 이 코드는 최종 SQL 쿼리를 보간해야 하는 일부 사용 사례에 사용될 수 있습니다.

$queryString 속성은 전달된 쿼리만 반환하고 파라미터는 값으로 대체하지 않습니다..Net에서는 쿼리 실행자의 캐치 부분이 파라미터에 대해 제공된 값으로 단순 검색 치환을 수행하도록 합니다.이것에 의해, 에러 로그에 쿼리에 사용되고 있던 실제의 값이 표시됩니다.PHP에서 매개 변수를 열거하고 매개 변수를 할당된 값으로 바꿀 수 있습니다.

이 질문이 좀 오래된 건 알지만, 예전부터 이 코드를 사용하고 있습니다(@chris-go로부터의 응답을 사용하고 있습니다).이러한 코드는 PHP 7.2에서는 사용되지 않습니다.

이 코드의 최신 버전을 게시합니다(메인 코드의 크레딧은 @bigwebguy, @mike@chris-go이며, 모두 이 질문에 대한 답변입니다).

/**
 * Replaces any parameter placeholders in a query with the value of that
 * parameter. Useful for debugging. Assumes anonymous parameters from 
 * $params are are in the same order as specified in $query
 *
 * @param string $query The sql query with parameter placeholders
 * @param array $params The array of substitution parameters
 * @return string The interpolated query
 */
public function interpolateQuery($query, $params) {
    $keys = array();
    $values = $params;

    # build a regular expression for each parameter
    foreach ($params as $key => $value) {
        if (is_string($key)) {
            $keys[] = '/:'.$key.'/';
        } else {
            $keys[] = '/[?]/';
        }

        if (is_array($value))
            $values[$key] = implode(',', $value);

        if (is_null($value))
            $values[$key] = 'NULL';
    }
    // Walk the array to see if we can add single-quotes to strings
    array_walk($values, function(&$v, $k) { if (!is_numeric($v) && $v != "NULL") $v = "\'" . $v . "\'"; });

    $query = preg_replace($keys, $values, $query, 1, $count);

    return $query;
}

코드의 변경은 array_walk() 함수로 create_function을 어나니머스 함수로 대체합니다.이것에 의해, 이러한 코드의 일부가 기능해, PHP 7.2 와 호환되게 됩니다(향후 버전도 희망합니다).

preg_replace는 기능하지 않습니다.binding_1과 binding_10이 9를 넘었을 때 binding_1과 binding_10이 str_replace로 대체되었기 때문에 (0을 뒤로 한 채) 치환했습니다.

public function interpolateQuery($query, $params) {
$keys = array();
    $length = count($params)-1;
    for ($i = $length; $i >=0; $i--) {
            $query  = str_replace(':binding_'.(string)$i, '\''.$params[$i]['val'].'\'', $query);
           }
        // $query  = str_replace('SQL_CALC_FOUND_ROWS', '', $query, $count);
        return $query;

}

누군가 유용하게 써주길 바라.

bind param 뒤에 완전한 쿼리 문자열을 기록해야 하기 때문에 이것은 내 코드에 포함되어 있습니다.Hope, 그것은 모든 사람에게 같은 문제를 가지고 있습니다.

/**
 * 
 * @param string $str
 * @return string
 */
public function quote($str) {
    if (!is_array($str)) {
        return $this->pdo->quote($str);
    } else {
        $str = implode(',', array_map(function($v) {
                    return $this->quote($v);
                }, $str));

        if (empty($str)) {
            return 'NULL';
        }

        return $str;
    }
}

/**
 * 
 * @param string $query
 * @param array $params
 * @return string
 * @throws Exception
 */
public function interpolateQuery($query, $params) {
    $ps = preg_split("/'/is", $query);
    $pieces = [];
    $prev = null;
    foreach ($ps as $p) {
        $lastChar = substr($p, strlen($p) - 1);

        if ($lastChar != "\\") {
            if ($prev === null) {
                $pieces[] = $p;
            } else {
                $pieces[] = $prev . "'" . $p;
                $prev = null;
            }
        } else {
            $prev .= ($prev === null ? '' : "'") . $p;
        }
    }

    $arr = [];
    $indexQuestionMark = -1;
    $matches = [];

    for ($i = 0; $i < count($pieces); $i++) {
        if ($i % 2 !== 0) {
            $arr[] = "'" . $pieces[$i] . "'";
        } else {
            $st = '';
            $s = $pieces[$i];
            while (!empty($s)) {
                if (preg_match("/(\?|:[A-Z0-9_\-]+)/is", $s, $matches, PREG_OFFSET_CAPTURE)) {
                    $index = $matches[0][1];
                    $st .= substr($s, 0, $index);
                    $key = $matches[0][0];
                    $s = substr($s, $index + strlen($key));

                    if ($key == '?') {
                        $indexQuestionMark++;
                        if (array_key_exists($indexQuestionMark, $params)) {
                            $st .= $this->quote($params[$indexQuestionMark]);
                        } else {
                            throw new Exception('Wrong params in query at ' . $index);
                        }
                    } else {
                        if (array_key_exists($key, $params)) {
                            $st .= $this->quote($params[$key]);
                        } else {
                            throw new Exception('Wrong params in query with key ' . $key);
                        }
                    }
                } else {
                    $st .= $s;
                    $s = null;
                }
            }
            $arr[] = $st;
        }
    }

    return implode('', $arr);
}

Mike의 답변은 "재사용" 바인드 값을 사용할 때까지 유효합니다.
예를 들어 다음과 같습니다.

SELECT * FROM `an_modules` AS `m` LEFT JOIN `an_module_sites` AS `ms` ON m.module_id = ms.module_id WHERE 1 AND `module_enable` = :module_enable AND `site_id` = :site_id AND (`module_system_name` LIKE :search OR `module_version` LIKE :search)

Mike의 답변은 첫 번째 검색만 대체할 수 있으며 두 번째 검색은 대체할 수 없습니다.
적절하게 재사용할 수 있는 여러 파라미터로 작업하도록 답변을 다시 씁니다.

public function interpolateQuery($query, $params) {
    $keys = array();
    $values = $params;
    $values_limit = [];

    $words_repeated = array_count_values(str_word_count($query, 1, ':_'));

    # build a regular expression for each parameter
    foreach ($params as $key => $value) {
        if (is_string($key)) {
            $keys[] = '/:'.$key.'/';
            $values_limit[$key] = (isset($words_repeated[':'.$key]) ? intval($words_repeated[':'.$key]) : 1);
        } else {
            $keys[] = '/[?]/';
            $values_limit = [];
        }

        if (is_string($value))
            $values[$key] = "'" . $value . "'";

        if (is_array($value))
            $values[$key] = "'" . implode("','", $value) . "'";

        if (is_null($value))
            $values[$key] = 'NULL';
    }

    if (is_array($values)) {
        foreach ($values as $key => $val) {
            if (isset($values_limit[$key])) {
                $query = preg_replace(['/:'.$key.'/'], [$val], $query, $values_limit[$key], $count);
            } else {
                $query = preg_replace(['/:'.$key.'/'], [$val], $query, 1, $count);
            }
        }
        unset($key, $val);
    } else {
        $query = preg_replace($keys, $values, $query, 1, $count);
    }
    unset($keys, $values, $values_limit, $words_repeated);

    return $query;
}

언급URL : https://stackoverflow.com/questions/210564/getting-raw-sql-query-string-from-pdo-prepared-statements