Skip to content

Instantly share code, notes, and snippets.

@imanilchaudhari
Created May 3, 2018 06:27
Show Gist options
  • Save imanilchaudhari/3cfcd7d3355b30a3ba1200ae781350a8 to your computer and use it in GitHub Desktop.
Save imanilchaudhari/3cfcd7d3355b30a3ba1200ae781350a8 to your computer and use it in GitHub Desktop.
Yii2 active record compatible with geometrical data types.
<?php
namespace common\components;
use Yii;
use yii\db\Expression;
use yii\db\ActiveRecord as YiiActiveRecord;
use yii\base\InvalidCallException;
/**
* Class providing additional features for yii\db\ActiveRecord. Add support for JSON and spatial data types.
*
* @since 0.0.1
*/
class ActiveRecord extends YiiActiveRecord
{
/**
* @since 0.0.1
* @var float Property for distance between geometrical points
*/
public $_d;
protected $_saved = [];
/**
* {@inheritDoc}
*/
public static function find() {
return Yii::createObject(ActiveQuery::className(), [get_called_class()]);
}
/**
* Determine whether given `$column` is of spatial data type POINT
*
* @since 0.0.1
* @param string $column The column to check
* @return boolean
*/
public static function isPoint($column) {
return $column ? ($column->dbType == 'point') : false;
}
/**
* Determine whether given `$column` is of spatial data type POLYGON
*
* @since 0.0.1
* @param string $column The column to check
* @return boolean
*/
public static function isPolygon($column) {
return $column ? ($column->dbType == "polygon") : false;
}
/**
* Determine whether given `$column` is of data type JSON
*
* @since 0.0.1
* @param string $column
* @return boolean
*/
public static function isJson($column) {
return $column ? ($column->dbType == "json") : false;
}
/**
* Convert WKT to point value.
*
* @since 0.0.1
* @param string $val WKT to convert
* @return Expression
* @link https://postgis.net/docs/ST_PointFromText.html
*/
protected function toDB($val) {
$driver = $this->db->driverName;
$exp = $val;
if ($driver === 'mysql') {
$pnt = str_replace(',', ' ', $val);
$exp = new Expression("ST_PointFromText('POINT($pnt)')");
}
return $exp;
}
/**
* Convert geometric values back to more readable form.
*
* For example, converts DB value of `POINT(56.7 53.34)` to `56.7,53.34`. In case of POLYGON, converts `POLYGON((0 0,0 3,3 0,0 0),(1 1,1 2,2 1,1 1))` to
* ```php
* [
* [
* [
* [x] => 0,
* [y] => 0
* ],
* [
* [x] => 0,
* [y] => 3
* ],
* [
* [x] => 3,
* [y] => 0
* ],
* [
* [x] => 0,
* [y] => 0
* ]
* ],
* [
* [
* [x] => 1,
* [y] => 1
* ],
* [
* [x] => 1,
* [y] => 2
* ],
* [
* [x] => 2,
* [y] => 1
* ],
* [
* [x] => 1,
* [y] => 1
* ]
* ]
* ]
* ```
*
* @todo Improve regex patterns
* @since 0.0.1
* @param string $exp The value to convert.
* @param string $geometry The type of geometry. Defaults to `point`.
* @return mixed
*/
protected function toAttr($exp, $geometry = "point") {
$driver = $this->db->driverName;
$val = $exp;
if ($geometry === "point" && preg_match('#\((.*?)\)#', strtoupper($exp), $matches)) {
$val = $matches[1];
if ($driver === 'mysql')
$val = str_replace(' ', ',', $val);
}
if ($geometry === "polygon" && preg_match("/^POLYGON\(\((.*?)\)+\)$/", strtoupper($exp), $matches)) {
$val = $matches[1];
if ($driver === "mysql" && strpos($val, "),(") !== false) {
$parts = explode("),(", $val);
array_walk($parts, [$this, "demolishPolygon"]);
$val = $parts;
}
}
return $val;
}
/**
* Helper method to convert geometrical data to more workable form.
*
* @since 0.0.1
* @param string $item
* @param string $key
*/
protected function demolishPolygon(&$item, $key) {
$parts = explode(",", $item);
array_walk($parts, [$this, "coordinatesToArray"]);
$item = $parts;
}
/**
* Helper method to convert coordinates to array.
*
* @since 0.0.1
* @param array $item The item to convert to array.
*/
protected function coordinatesToArray(&$item) {
$parts = explode(" ", $item);
$item = ["x" => $parts[0], "y" => $parts[1]];
}
protected function viewportToPolygon($viewport) {
return "POLYGON((".$viewport->west." ".$viewport->south.",".$viewport->west." ".$viewport->north.",".$viewport->east." ".$viewport->north.",".$viewport->east." ".$viewport->south.",".$viewport->west." ".$viewport->south."))";
}
/**
* Convert specific attributes like JSON or spatial data to appropriate format to be saved in the DB.
*
* @since 0.0.1
* @param object $insert
* @return object
*/
public function beforeSave($insert) {
$r = parent::beforeSave($insert);
if ($r) {
$scheme = static::getTableSchema();
foreach ($scheme->columns as $column) {
if (static::isPoint($column)) {
$field = $column->name;
$value = $this->getAttribute($field);
if ($value) {
$this->_saved[$field] = $value;
$exp = $this->toDB($value);
$this->setAttribute($field, $exp);
}
}
if (static::isJson($column)) {
$field = $column->name;
$value = $this->getAttribute($field);
//Check whether current attribute belongs to current scenario. If not, change it's value back to what it was.
//This is required due to the fact that in afterFind() all JSON type attributes are altered for the sake of
//easier usage.
if (in_array($field, array_keys($this->getDirtyAttributes())) && !in_array($field, $this->activeAttributes())) {
$this->{$field} = $this->oldAttributes[$field];
continue;
}
//Remove the second part of comparison if something should fail. Added just for cases where input field is left empty. Otherwise would fail with exception.
if ($value || $value == "") {
$this->_saved[$field] = $value;
// $encoded = json_encode($value);
$this->setAttribute($field, $value);
}
}
}
}
return $r;
}
/**
* Restore specific attributes format as it was before saving to DB.
*
* @since 0.0.1
* @param object $insert
* @param object $changedAttributes
*/
public function afterSave($insert, $changedAttributes) {
foreach ($this->_saved as $field => $value)
$this->setAttribute($field, $value);
parent::afterSave($insert, $changedAttributes);
}
/**
* Convert specific attributes like JSON or spatial data types to more usable format.
*
* @since 0.0.1
* @throws InvalidCallException
*/
public function afterFind() {
parent::afterFind();
$scheme = static::getTableSchema();
foreach ($scheme->columns as $column) {
if (static::isPoint($column)) {
$field = $column->name;
$value = $this->getAttribute($field);
if ($value) {
if (YII_DEBUG && preg_match( '/[\\x80-\\xff]+/' , $value )) {
throw new InvalidCallException('Spatial attribute not converted.');
}
$text = $this->toAttr($value);
$this->setAttribute($field, $text);
}
}
if (static::isPolygon($column)) {
$field = $column->name;
$value = $this->getAttribute($field);
if ($value) {
if (YII_DEBUG && preg_match( '/[\\x80-\\xff]+/' , $value )) {
throw new InvalidCallException('Spatial attribute not converted.');
}
$text = $this->toAttr($value, "polygon");
$this->setAttribute($field, $text);
}
}
//Add conversion for JSON data types
if (static::isJson($column)) {
$field = $column->name;
$value = $this->getAttribute($field);
if ($value) {
// $decoded = json_decode($value, true);
$this->setAttribute($field, $value);
}
}
}
}
public function getDistance()
{
return $this->_d;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment