From 14a3bf04cef6225415ccfa11c47f4f9164c16ef6 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 24 Jul 2017 11:37:30 +0200 Subject: [PATCH 01/21] Extract DoctrineBaseDriver getSearchQueryBuilder method from search --- Component/Drivers/DoctrineBaseDriver.php | 50 ++++++++++++++---------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/Component/Drivers/DoctrineBaseDriver.php b/Component/Drivers/DoctrineBaseDriver.php index dd7d5b9..a4b9dec 100644 --- a/Component/Drivers/DoctrineBaseDriver.php +++ b/Component/Drivers/DoctrineBaseDriver.php @@ -244,27 +244,7 @@ public function getSelectQueryBuilder(array $fields = array()) */ public function search(array $criteria = array()) { - - /** @var Statement $statement */ - $maxResults = isset($criteria['maxResults']) ? intval($criteria['maxResults']) : self::MAX_RESULTS; - $where = isset($criteria['where']) ? $criteria['where'] : null; - $queryBuilder = $this->getSelectQueryBuilder(); - // $returnType = isset($criteria['returnType']) ? $criteria['returnType'] : null; - - // add filter (https://trac.wheregroup.com/cp/issues/3733) - if (!empty($this->sqlFilter)) { - $queryBuilder->andWhere($this->sqlFilter); - } - - // add second filter (https://trac.wheregroup.com/cp/issues/4643) - if ($where) { - $queryBuilder->andWhere($where); - } - - $queryBuilder->setMaxResults($maxResults); - // $queryBuilder->setParameters($params); - $statement = $queryBuilder->execute(); - $rows = $statement->fetchAll(); + $rows = $this->getSearchQueryBuilder($criteria)->execute()->fetchAll(); $hasResults = count($rows) > 0; // Cast array to DataItem array list @@ -439,6 +419,34 @@ public function getLastInsertId() return $this->getConnection()->lastInsertId(); } + /** + * Get search query builder + * + * @param array $criteria + * @return QueryBuilder + */ + public function getSearchQueryBuilder(array $criteria) + { + /** @var Statement $statement */ + $maxResults = isset($criteria['maxResults']) ? intval($criteria['maxResults']) : self::MAX_RESULTS; + $where = isset($criteria['where']) ? $criteria['where'] : null; + $queryBuilder = $this->getSelectQueryBuilder(); + // $returnType = isset($criteria['returnType']) ? $criteria['returnType'] : null; + + // add filter (https://trac.wheregroup.com/cp/issues/3733) + if (!empty($this->sqlFilter)) { + $queryBuilder->andWhere($this->sqlFilter); + } + + // add second filter (https://trac.wheregroup.com/cp/issues/4643) + if ($where) { + $queryBuilder->andWhere($where); + } + + $queryBuilder->setMaxResults($maxResults); + return $queryBuilder; + } + /** * Extract ordered type list from two associate key lists of data and types. * From 036a6444a65885c6287cd1d30b701cd3e5cfc56b Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 24 Jul 2017 11:37:56 +0200 Subject: [PATCH 02/21] Add spatialite driver --- Component/Drivers/SQLite.php | 132 ++++++++++++++++++++++++++++++++++- composer.json | 1 + 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/Component/Drivers/SQLite.php b/Component/Drivers/SQLite.php index a772d9b..53f9d3e 100644 --- a/Component/Drivers/SQLite.php +++ b/Component/Drivers/SQLite.php @@ -1,14 +1,36 @@ */ -class SQLite extends DoctrineBaseDriver +class SQLite extends PostgreSQL implements Geographic { + /** + * Get spatial driver instance + * + * @return SpatialiteShellDriver|null + */ + public function getSpatialDriver() + { + static $driver = null; + + if (!$driver) { + $dbPath = isset($this->settings['path']) ? $this->settings['path'] : $this->container->get('kernel')->getRootDir() . "/app/db/"; + $driver = new SpatialiteShellDriver($dbPath); + } + + return $driver; + } + /** * Get table fields * @@ -29,4 +51,112 @@ public function getStoreFields() } return $columns; } + + /** + * @inheritdoc + */ + public function search(array $criteria = array()) + { + $sql = $this->getSearchQueryBuilder($criteria)->getSQL(); + $rows = $this->getSpatialDriver()->query($sql); + $hasResults = count($rows) > 0; + + // Cast array to DataItem array list + if ($hasResults) { + $this->prepareResults($rows); + } + + return $rows; + } + + /** + * Add geometry column + * + * @param $tableName + * @param $type + * @param $srid + * @param string $geomFieldName + * @param string $schemaName + * @param int $dimensions + * @return mixed + */ + public function addGeometryColumn($tableName, + $type, + $srid, + $geomFieldName = "geom", + $schemaName = "public", + $dimensions = 2) + { + $spatialDriver = $this->getSpatialDriver(); + return $spatialDriver->addGeometryColumn($tableName, $geomFieldName, $srid, $type); + } + + /** + * Get table geometry type + * + * @param $tableName + * @param string $schema + * @return mixed + */ + public function getTableGeomType($tableName, $schema = null) + { + $connection = $this->getSpatialDriver(); + if (strpos($tableName, '.')) { + list($schema, $tableName) = explode('.', $tableName); + } + $_schema = $schema ? $connection->quote($schema) : 'current_schema()'; + + $type = $connection->query("SELECT \"type\" + FROM geometry_columns + WHERE f_table_schema = " . $_schema . " + AND f_table_name = " . $connection->quote($tableName))->fetchColumn(); + return $type; + } + + /** + * @param $ewkt + * @param null $srid + * @return mixed + * @internal param $wkt + */ + public function transformEwkt($ewkt, $srid = null) + { + $db = $this->getSpatialDriver(); + $type = $this->getTableGeomType($this->getTableName()); + $wktType = static::getWktType($ewkt); + + if ($type + && $wktType != $type + && in_array(strtoupper($wktType), Feature::$simpleGeometries) + && in_array(strtoupper($type), Feature::$complexGeometries) + ) { + $ewkt = 'SRID=' . $srid . ';' . $db->fetchColumn("SELECT ST_ASTEXT(ST_TRANSFORM(ST_MULTI(" . $db->quote($ewkt) . "),$srid))"); + } + + $srid = is_numeric($srid) ? intval($srid) : $db->quote($srid); + $ewkt = $db->quote($ewkt); + + return $db->fetchColumn("SELECT ST_TRANSFORM(ST_GEOMFROMTEXT($ewkt), $srid)"); + } + + /** + * Get WKB geometry attribute as WKT + * + * @param string $tableName + * @param string $geomFieldName + * @return string SQL + */ + public function findGeometryFieldSrid($tableName, $geomFieldName) + { + $connection = $this->getSpatialDriver(); + $schemaName = "current_schema()"; + if (strpos($tableName, ".")) { + list($schemaName, $tableName) = explode('.', $tableName); + $schemaName = $connection->quote($schemaName); + } + + return $connection->fetchColumn("SELECT Find_SRID(" . $schemaName . ", + " . $connection->quote($tableName) . ", + " . $connection->quote($geomFieldName) . ")"); + } } \ No newline at end of file diff --git a/composer.json b/composer.json index 7ad9a44..a2007bf 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ }, "require-dev": { "phpunit/phpunit": "^3.7", + "eslider/spatialite": "0.x", "symfony/framework-bundle" :"*" }, "config": { From adda59873238b672e58d487c57587bf33407779c Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 24 Jul 2017 11:51:44 +0200 Subject: [PATCH 03/21] Requiere spatialite driver --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index a2007bf..bb2740c 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "require": { "php": ">=5.3.3", "zumba/json-serializer": "1.x", + "eslider/spatialite": "0.x", "phayes/geophp": "1.2" }, "require-dev": { From 197cc834356cbc5fca667c62169f0c81f53ceb21 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 24 Jul 2017 13:34:04 +0200 Subject: [PATCH 04/21] Improve fill method of BaseConfiguration entity to get parse annotations --- Entity/BaseConfiguration.php | 104 ++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 14 deletions(-) diff --git a/Entity/BaseConfiguration.php b/Entity/BaseConfiguration.php index da1345b..6f7f4f3 100644 --- a/Entity/BaseConfiguration.php +++ b/Entity/BaseConfiguration.php @@ -1,4 +1,5 @@ fill($args); + if ($data) { + if (isset($data['@attributes'])) { + $this->fill($data['@attributes']); + } + $this->fill($data); } } /** - * Fill object with values from $args - * - * @param $args + * @param array $data */ - public function fill($args) + public function fill(array &$data) { - foreach (get_object_vars($this) as $key => $value) { - foreach ($args as $argKey => $argValue) { - if ($key == $argKey || $key == $argKey . "Name") { - $this->$key = $argValue; + static $className, $methods, $vars, $reflection; + + if (!$className) { + $className = get_class($this); + $methods = get_class_methods($className); + $vars = array_keys(get_class_vars($className)); + $reflection = new \ReflectionClass($className); + } + + foreach ($data as $k => $v) { + if ($k == "@attributes") { + continue; + } + + $methodName = 'set' . ucfirst($k); + if (in_array($methodName, $methods)) { + $this->{$methodName}($v); + continue; + } + + $methodName = 'set' . ucfirst($this->removeNameSpaceFromVariableName($k)); + if (in_array($methodName, $methods)) { + $this->{$methodName}($v); + continue; + } + + $varName = lcfirst($this->removeNameSpaceFromVariableName($k)); + if (in_array($varName, $vars)) { + $docComment = $reflection->getProperty($varName)->getDocComment(); + if (preg_match('/@var ([\\\]?[A-Z]\S+)/s', $docComment, $annotations)) { + $varClassName = $annotations[1]; + if (class_exists($varClassName)) { + $v = new $varClassName($v); + } + } + $this->{$varName} = $v; + continue; + } + + $varName .= "s"; + if (in_array($varName, $vars)) { + $docComment = $reflection->getProperty($varName)->getDocComment(); + if ($annotations = self::parse('/@var\s+([\\\]?[A-Z]\S+)(\[\])/s', $docComment)) { + $varClassName = $annotations[1]; + if (class_exists($varClassName)) { + $items = array(); + $isNumeric = is_int(key($v)); + $list = $isNumeric ? $v : array($v); + foreach ($list as $subData) { + $items[] = new $varClassName($subData); + } + $v = $items; + } } + $this->{$varName} = $v; + continue; } } } /** - * Export + * @param $name + * @return mixed + */ + private function removeNameSpaceFromVariableName($name) + { + return preg_replace("/^.+?_/", '', $name); + } + + /** + * Export data as array */ public function toArray() { return get_object_vars($this); } + + /** + * Parse string + * + * @param string $reg regular expression + * @param string $str + * @return null + */ + private static function parse($reg, $str) + { + $annotations = null; + preg_match($reg, $str, $annotations); + return $annotations; + } } \ No newline at end of file From 8772328b4e5da97cc152453ce21578643442f261 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 31 Jul 2017 12:23:57 +0200 Subject: [PATCH 05/21] Add getGeomAttributeAsJson method to PostgreSQL --- Component/Drivers/PostgreSQL.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Component/Drivers/PostgreSQL.php b/Component/Drivers/PostgreSQL.php index d618ee2..3755ae2 100644 --- a/Component/Drivers/PostgreSQL.php +++ b/Component/Drivers/PostgreSQL.php @@ -326,6 +326,17 @@ public function getGeomAttributeAsWkt($geometryAttribute, $sridTo) return "ST_ASTEXT(ST_TRANSFORM($geomFieldName, $sridTo)) AS $geomFieldName"; } + /** + * @inheritdoc + */ + public function getGeomAttributeAsJson($geometryAttribute, $sridTo) + { + $connection = $this->getConnection(); + $geomFieldName = $connection->quoteIdentifier($geometryAttribute); + $sridTo = is_numeric($sridTo)?intval($sridTo):$connection->quote($sridTo); + return "ST_AsGeoJSON(ST_TRANSFORM($geomFieldName, $sridTo)) AS $geomFieldName"; + } + /** * @inheritdoc */ From 25a122ab60197900ad258822ce69940430a58935 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 28 Aug 2017 11:41:34 +0200 Subject: [PATCH 06/21] Set DataStore to use ContainerAwareTrait instead of ContainerAware --- Component/DataStore.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Component/DataStore.php b/Component/DataStore.php index 9a854f5..121c931 100644 --- a/Component/DataStore.php +++ b/Component/DataStore.php @@ -13,6 +13,7 @@ use Mapbender\DataSourceBundle\Entity\DataItem; use Symfony\Component\Config\Definition\Exception\Exception; use Symfony\Component\DependencyInjection\ContainerAware; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -23,8 +24,10 @@ * @package Mapbender\DataSourceBundle * @author Andriy Oblivantsev */ -class DataStore extends ContainerAware +class DataStore { + use ContainerAwareTrait; + const ORACLE_PLATFORM = 'oracle'; const POSTGRESQL_PLATFORM = 'postgresql'; const SQLITE_PLATFORM = 'sqlite'; From 9a74058a93d31148e3d8cc99437baed820be4f52 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Wed, 30 Aug 2017 18:08:21 +0200 Subject: [PATCH 07/21] Get request object from symfony2 request_stack --- Controller/BaseController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Controller/BaseController.php b/Controller/BaseController.php index 76a1de6..1d4186c 100644 --- a/Controller/BaseController.php +++ b/Controller/BaseController.php @@ -15,7 +15,7 @@ class BaseController extends Controller */ protected function getRequestData() { - $content = $this->getRequest()->getContent(); + $content = $this->get('request_stack')->getCurrentRequest()->getContent(); $request = array_merge($_POST, $_GET); if (!empty($content)) { $request = array_merge($request, json_decode($content, true)); From e79c6204e5082cf97133beb514ff51e28a439582 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 4 Sep 2017 14:37:08 +0200 Subject: [PATCH 08/21] Add fallback for save feature, if auto generating ID fails --- Component/Drivers/DoctrineBaseDriver.php | 13 +++++++++++++ Component/FeatureType.php | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Component/Drivers/DoctrineBaseDriver.php b/Component/Drivers/DoctrineBaseDriver.php index a4b9dec..f5fffcc 100644 --- a/Component/Drivers/DoctrineBaseDriver.php +++ b/Component/Drivers/DoctrineBaseDriver.php @@ -467,4 +467,17 @@ protected function extractTypeValues(array $data, array $types) return $typeValues; } + + /** + * Return next possible ID + * + * @return mixed + */ + public function getNextPossibleId() + { + $con = $this->connection; + return $con->fetchColumn('SELECT MAX(' + . $con->quoteIdentifier($this->getUniqueId()) + . ")+1 FROM " . $con->quoteIdentifier($this->getTableName())); + } } \ No newline at end of file diff --git a/Component/FeatureType.php b/Component/FeatureType.php index e748ef7..c1d9221 100644 --- a/Component/FeatureType.php +++ b/Component/FeatureType.php @@ -6,6 +6,7 @@ use Doctrine\ORM\Mapping as ORM; use Mapbender\CoreBundle\Component\Application as AppComponent; use Mapbender\DataSourceBundle\Component\Drivers\BaseDriver; +use Mapbender\DataSourceBundle\Component\Drivers\DoctrineBaseDriver; use Mapbender\DataSourceBundle\Component\Drivers\Interfaces\Geographic; use Mapbender\DataSourceBundle\Component\Drivers\Oracle; use Mapbender\DataSourceBundle\Component\Drivers\PostgreSQL; @@ -191,7 +192,17 @@ public function save($featureData, $autoUpdate = true) if ($this->allowSave) { // Insert if no ID given if (!$autoUpdate || !$feature->hasId()) { - $feature = $this->insert($feature); + try{ + $feature = $this->insert($feature); + } catch (\Exception $e){ + // Fallback, if can't save, course no auto ID set + // then try it to set it manuel and try to save it one more time. + $driver = $this->getDriver(); + if($driver instanceof DoctrineBaseDriver){ + $feature->setId($driver->getNextPossibleId()); + $feature = $this->insert($feature); + } + } } // Replace if has ID else { $feature = $this->update($feature); @@ -313,6 +324,8 @@ public function update($featureData) * * @param array $criteria * @return Feature[] + * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + * @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException */ public function search(array $criteria = array()) { From 723a2b0e7e7fa3f5c3a202107edf2b6ea992cc22 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Thu, 7 Sep 2017 10:04:12 +0200 Subject: [PATCH 09/21] Optimize import of DataStore.php --- Component/DataStore.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Component/DataStore.php b/Component/DataStore.php index 121c931..5c64f56 100644 --- a/Component/DataStore.php +++ b/Component/DataStore.php @@ -3,7 +3,6 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Statement; -use FOM\UserBundle\Entity\User; use Mapbender\DataSourceBundle\Component\Drivers\BaseDriver; use Mapbender\DataSourceBundle\Component\Drivers\DoctrineBaseDriver; use Mapbender\DataSourceBundle\Component\Drivers\Interfaces\Base; @@ -12,7 +11,6 @@ use Mapbender\DataSourceBundle\Component\Drivers\YAML; use Mapbender\DataSourceBundle\Entity\DataItem; use Symfony\Component\Config\Definition\Exception\Exception; -use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerInterface; From 1f1dafa5a3bc6f3e8d8dff41cbb9a1891e647bd9 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Wed, 13 Dec 2017 21:42:25 +0100 Subject: [PATCH 10/21] Fix decode multipart request content --- Element/BaseElement.php | 82 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/Element/BaseElement.php b/Element/BaseElement.php index 92927e8..be85961 100644 --- a/Element/BaseElement.php +++ b/Element/BaseElement.php @@ -3,7 +3,6 @@ use Doctrine\DBAL\Connection; use Mapbender\CoreBundle\Element\HTMLElement; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Zumba\Util\JsonSerializer; @@ -126,6 +125,71 @@ protected function prepareItem($item) return $item; } + /** + * + * Parse raw HTTP request data + * + * Pass in $a_data as an array. This is done by reference to avoid copying + * the data around too much. + * + * Any files found in the request will be added by their field name to the + * $data['files'] array. + * + * @see http://www.chlab.ch/blog/archives/webdevelopment/manually-parse-raw-http-data-php + * @param array Empty array to fill with data + * @return array Associative array of request data + */ + public static function parseMultiPartRequest($content) + { + $result = array(); + // read incoming data + + // grab multipart boundary from content type header + preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); + + // content type is probably regular form-encoded + if (!count($matches)) { + // we expect regular puts to containt a query string containing data + parse_str(urldecode($content), $result); + return $result; + } + + $boundary = $matches[1]; + + // split content by boundary and get rid of last -- element + $a_blocks = preg_split("/-+$boundary/", $content); + array_pop($a_blocks); + + // loop data blocks + foreach ($a_blocks as $id => $block) { + if (empty($block)) { + continue; + } + + // you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char + + // parse uploaded files + if (strpos($block, 'application/octet-stream') !== false) { + // match "name", then everything after "stream" (optional) except for prepending newlines + preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); + $keyName = preg_replace('/\[\]$/', '', $matches[1]); + + if (!isset($result[ $keyName ])) { + $result[ $keyName ] = array(); + } + $result[ $keyName ][] = $matches[2]; + + } // parse all other fields + else { + // match "name" and optional value in between newline sequences + preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); + $result[ $matches[1] ] = $matches[2]; + } + } + + return $result; + } + /** * @return array|mixed * @throws \LogicException @@ -134,11 +198,17 @@ protected function prepareItem($item) */ protected function getRequestData() { - $content = $this->container->get('request')->getContent(); - $request = array_merge($_POST, $_GET); - - if (!empty($content)) { - $request = array_merge($request, json_decode($content, true)); + $content = $this->container->get('request')->getContent(); + $request = array_merge($_POST, $_GET); + $hasContent = !empty($content); + + if ($hasContent) { + $isMultipart = strpos($content, '-') === 0; + if ($isMultipart) { + $request = array_merge($request, static::parseMultiPartRequest($content)); + } else { + $request = array_merge($request, json_decode($content, true)); + } } return $this->decodeRequest($request); From 84988f3720796acfc870334f3d066e862c392ed3 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 24 Jul 2017 11:37:30 +0200 Subject: [PATCH 11/21] Extract DoctrineBaseDriver getSearchQueryBuilder method from search --- Component/Drivers/DoctrineBaseDriver.php | 50 ++++++++++++++---------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/Component/Drivers/DoctrineBaseDriver.php b/Component/Drivers/DoctrineBaseDriver.php index dd7d5b9..a4b9dec 100644 --- a/Component/Drivers/DoctrineBaseDriver.php +++ b/Component/Drivers/DoctrineBaseDriver.php @@ -244,27 +244,7 @@ public function getSelectQueryBuilder(array $fields = array()) */ public function search(array $criteria = array()) { - - /** @var Statement $statement */ - $maxResults = isset($criteria['maxResults']) ? intval($criteria['maxResults']) : self::MAX_RESULTS; - $where = isset($criteria['where']) ? $criteria['where'] : null; - $queryBuilder = $this->getSelectQueryBuilder(); - // $returnType = isset($criteria['returnType']) ? $criteria['returnType'] : null; - - // add filter (https://trac.wheregroup.com/cp/issues/3733) - if (!empty($this->sqlFilter)) { - $queryBuilder->andWhere($this->sqlFilter); - } - - // add second filter (https://trac.wheregroup.com/cp/issues/4643) - if ($where) { - $queryBuilder->andWhere($where); - } - - $queryBuilder->setMaxResults($maxResults); - // $queryBuilder->setParameters($params); - $statement = $queryBuilder->execute(); - $rows = $statement->fetchAll(); + $rows = $this->getSearchQueryBuilder($criteria)->execute()->fetchAll(); $hasResults = count($rows) > 0; // Cast array to DataItem array list @@ -439,6 +419,34 @@ public function getLastInsertId() return $this->getConnection()->lastInsertId(); } + /** + * Get search query builder + * + * @param array $criteria + * @return QueryBuilder + */ + public function getSearchQueryBuilder(array $criteria) + { + /** @var Statement $statement */ + $maxResults = isset($criteria['maxResults']) ? intval($criteria['maxResults']) : self::MAX_RESULTS; + $where = isset($criteria['where']) ? $criteria['where'] : null; + $queryBuilder = $this->getSelectQueryBuilder(); + // $returnType = isset($criteria['returnType']) ? $criteria['returnType'] : null; + + // add filter (https://trac.wheregroup.com/cp/issues/3733) + if (!empty($this->sqlFilter)) { + $queryBuilder->andWhere($this->sqlFilter); + } + + // add second filter (https://trac.wheregroup.com/cp/issues/4643) + if ($where) { + $queryBuilder->andWhere($where); + } + + $queryBuilder->setMaxResults($maxResults); + return $queryBuilder; + } + /** * Extract ordered type list from two associate key lists of data and types. * From c3471c3f063baa583fbf87c6ee4bf9d622048359 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 24 Jul 2017 11:37:56 +0200 Subject: [PATCH 12/21] Add spatialite driver --- Component/Drivers/SQLite.php | 132 ++++++++++++++++++++++++++++++++++- composer.json | 1 + 2 files changed, 132 insertions(+), 1 deletion(-) diff --git a/Component/Drivers/SQLite.php b/Component/Drivers/SQLite.php index a772d9b..53f9d3e 100644 --- a/Component/Drivers/SQLite.php +++ b/Component/Drivers/SQLite.php @@ -1,14 +1,36 @@ */ -class SQLite extends DoctrineBaseDriver +class SQLite extends PostgreSQL implements Geographic { + /** + * Get spatial driver instance + * + * @return SpatialiteShellDriver|null + */ + public function getSpatialDriver() + { + static $driver = null; + + if (!$driver) { + $dbPath = isset($this->settings['path']) ? $this->settings['path'] : $this->container->get('kernel')->getRootDir() . "/app/db/"; + $driver = new SpatialiteShellDriver($dbPath); + } + + return $driver; + } + /** * Get table fields * @@ -29,4 +51,112 @@ public function getStoreFields() } return $columns; } + + /** + * @inheritdoc + */ + public function search(array $criteria = array()) + { + $sql = $this->getSearchQueryBuilder($criteria)->getSQL(); + $rows = $this->getSpatialDriver()->query($sql); + $hasResults = count($rows) > 0; + + // Cast array to DataItem array list + if ($hasResults) { + $this->prepareResults($rows); + } + + return $rows; + } + + /** + * Add geometry column + * + * @param $tableName + * @param $type + * @param $srid + * @param string $geomFieldName + * @param string $schemaName + * @param int $dimensions + * @return mixed + */ + public function addGeometryColumn($tableName, + $type, + $srid, + $geomFieldName = "geom", + $schemaName = "public", + $dimensions = 2) + { + $spatialDriver = $this->getSpatialDriver(); + return $spatialDriver->addGeometryColumn($tableName, $geomFieldName, $srid, $type); + } + + /** + * Get table geometry type + * + * @param $tableName + * @param string $schema + * @return mixed + */ + public function getTableGeomType($tableName, $schema = null) + { + $connection = $this->getSpatialDriver(); + if (strpos($tableName, '.')) { + list($schema, $tableName) = explode('.', $tableName); + } + $_schema = $schema ? $connection->quote($schema) : 'current_schema()'; + + $type = $connection->query("SELECT \"type\" + FROM geometry_columns + WHERE f_table_schema = " . $_schema . " + AND f_table_name = " . $connection->quote($tableName))->fetchColumn(); + return $type; + } + + /** + * @param $ewkt + * @param null $srid + * @return mixed + * @internal param $wkt + */ + public function transformEwkt($ewkt, $srid = null) + { + $db = $this->getSpatialDriver(); + $type = $this->getTableGeomType($this->getTableName()); + $wktType = static::getWktType($ewkt); + + if ($type + && $wktType != $type + && in_array(strtoupper($wktType), Feature::$simpleGeometries) + && in_array(strtoupper($type), Feature::$complexGeometries) + ) { + $ewkt = 'SRID=' . $srid . ';' . $db->fetchColumn("SELECT ST_ASTEXT(ST_TRANSFORM(ST_MULTI(" . $db->quote($ewkt) . "),$srid))"); + } + + $srid = is_numeric($srid) ? intval($srid) : $db->quote($srid); + $ewkt = $db->quote($ewkt); + + return $db->fetchColumn("SELECT ST_TRANSFORM(ST_GEOMFROMTEXT($ewkt), $srid)"); + } + + /** + * Get WKB geometry attribute as WKT + * + * @param string $tableName + * @param string $geomFieldName + * @return string SQL + */ + public function findGeometryFieldSrid($tableName, $geomFieldName) + { + $connection = $this->getSpatialDriver(); + $schemaName = "current_schema()"; + if (strpos($tableName, ".")) { + list($schemaName, $tableName) = explode('.', $tableName); + $schemaName = $connection->quote($schemaName); + } + + return $connection->fetchColumn("SELECT Find_SRID(" . $schemaName . ", + " . $connection->quote($tableName) . ", + " . $connection->quote($geomFieldName) . ")"); + } } \ No newline at end of file diff --git a/composer.json b/composer.json index 7ad9a44..a2007bf 100644 --- a/composer.json +++ b/composer.json @@ -14,6 +14,7 @@ }, "require-dev": { "phpunit/phpunit": "^3.7", + "eslider/spatialite": "0.x", "symfony/framework-bundle" :"*" }, "config": { From 2fe42d21fa114bbb27089b523bb45468abbda64c Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 24 Jul 2017 11:51:44 +0200 Subject: [PATCH 13/21] Requiere spatialite driver --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index a2007bf..bb2740c 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "require": { "php": ">=5.3.3", "zumba/json-serializer": "1.x", + "eslider/spatialite": "0.x", "phayes/geophp": "1.2" }, "require-dev": { From 81a203184643d87b407e5362a5c94f8073d0fcb3 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 24 Jul 2017 13:34:04 +0200 Subject: [PATCH 14/21] Improve fill method of BaseConfiguration entity to get parse annotations --- Entity/BaseConfiguration.php | 104 ++++++++++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 14 deletions(-) diff --git a/Entity/BaseConfiguration.php b/Entity/BaseConfiguration.php index da1345b..6f7f4f3 100644 --- a/Entity/BaseConfiguration.php +++ b/Entity/BaseConfiguration.php @@ -1,4 +1,5 @@ fill($args); + if ($data) { + if (isset($data['@attributes'])) { + $this->fill($data['@attributes']); + } + $this->fill($data); } } /** - * Fill object with values from $args - * - * @param $args + * @param array $data */ - public function fill($args) + public function fill(array &$data) { - foreach (get_object_vars($this) as $key => $value) { - foreach ($args as $argKey => $argValue) { - if ($key == $argKey || $key == $argKey . "Name") { - $this->$key = $argValue; + static $className, $methods, $vars, $reflection; + + if (!$className) { + $className = get_class($this); + $methods = get_class_methods($className); + $vars = array_keys(get_class_vars($className)); + $reflection = new \ReflectionClass($className); + } + + foreach ($data as $k => $v) { + if ($k == "@attributes") { + continue; + } + + $methodName = 'set' . ucfirst($k); + if (in_array($methodName, $methods)) { + $this->{$methodName}($v); + continue; + } + + $methodName = 'set' . ucfirst($this->removeNameSpaceFromVariableName($k)); + if (in_array($methodName, $methods)) { + $this->{$methodName}($v); + continue; + } + + $varName = lcfirst($this->removeNameSpaceFromVariableName($k)); + if (in_array($varName, $vars)) { + $docComment = $reflection->getProperty($varName)->getDocComment(); + if (preg_match('/@var ([\\\]?[A-Z]\S+)/s', $docComment, $annotations)) { + $varClassName = $annotations[1]; + if (class_exists($varClassName)) { + $v = new $varClassName($v); + } + } + $this->{$varName} = $v; + continue; + } + + $varName .= "s"; + if (in_array($varName, $vars)) { + $docComment = $reflection->getProperty($varName)->getDocComment(); + if ($annotations = self::parse('/@var\s+([\\\]?[A-Z]\S+)(\[\])/s', $docComment)) { + $varClassName = $annotations[1]; + if (class_exists($varClassName)) { + $items = array(); + $isNumeric = is_int(key($v)); + $list = $isNumeric ? $v : array($v); + foreach ($list as $subData) { + $items[] = new $varClassName($subData); + } + $v = $items; + } } + $this->{$varName} = $v; + continue; } } } /** - * Export + * @param $name + * @return mixed + */ + private function removeNameSpaceFromVariableName($name) + { + return preg_replace("/^.+?_/", '', $name); + } + + /** + * Export data as array */ public function toArray() { return get_object_vars($this); } + + /** + * Parse string + * + * @param string $reg regular expression + * @param string $str + * @return null + */ + private static function parse($reg, $str) + { + $annotations = null; + preg_match($reg, $str, $annotations); + return $annotations; + } } \ No newline at end of file From 0715af6f46b25945a72aebe933df6e06829da726 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 31 Jul 2017 12:23:57 +0200 Subject: [PATCH 15/21] Add getGeomAttributeAsJson method to PostgreSQL --- Component/Drivers/PostgreSQL.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Component/Drivers/PostgreSQL.php b/Component/Drivers/PostgreSQL.php index d618ee2..3755ae2 100644 --- a/Component/Drivers/PostgreSQL.php +++ b/Component/Drivers/PostgreSQL.php @@ -326,6 +326,17 @@ public function getGeomAttributeAsWkt($geometryAttribute, $sridTo) return "ST_ASTEXT(ST_TRANSFORM($geomFieldName, $sridTo)) AS $geomFieldName"; } + /** + * @inheritdoc + */ + public function getGeomAttributeAsJson($geometryAttribute, $sridTo) + { + $connection = $this->getConnection(); + $geomFieldName = $connection->quoteIdentifier($geometryAttribute); + $sridTo = is_numeric($sridTo)?intval($sridTo):$connection->quote($sridTo); + return "ST_AsGeoJSON(ST_TRANSFORM($geomFieldName, $sridTo)) AS $geomFieldName"; + } + /** * @inheritdoc */ From 92bace9e5f821227ec483ce04d705de3299f9db9 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 28 Aug 2017 11:41:34 +0200 Subject: [PATCH 16/21] Set DataStore to use ContainerAwareTrait instead of ContainerAware --- Component/DataStore.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Component/DataStore.php b/Component/DataStore.php index 9a854f5..121c931 100644 --- a/Component/DataStore.php +++ b/Component/DataStore.php @@ -13,6 +13,7 @@ use Mapbender\DataSourceBundle\Entity\DataItem; use Symfony\Component\Config\Definition\Exception\Exception; use Symfony\Component\DependencyInjection\ContainerAware; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -23,8 +24,10 @@ * @package Mapbender\DataSourceBundle * @author Andriy Oblivantsev */ -class DataStore extends ContainerAware +class DataStore { + use ContainerAwareTrait; + const ORACLE_PLATFORM = 'oracle'; const POSTGRESQL_PLATFORM = 'postgresql'; const SQLITE_PLATFORM = 'sqlite'; From e2c11d4aa5a67a43304252010e0bfc53465e92c0 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Wed, 30 Aug 2017 18:08:21 +0200 Subject: [PATCH 17/21] Get request object from symfony2 request_stack --- Controller/BaseController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Controller/BaseController.php b/Controller/BaseController.php index 76a1de6..1d4186c 100644 --- a/Controller/BaseController.php +++ b/Controller/BaseController.php @@ -15,7 +15,7 @@ class BaseController extends Controller */ protected function getRequestData() { - $content = $this->getRequest()->getContent(); + $content = $this->get('request_stack')->getCurrentRequest()->getContent(); $request = array_merge($_POST, $_GET); if (!empty($content)) { $request = array_merge($request, json_decode($content, true)); From ae9c84d7667386b67cbf809c25dc227019d59815 Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Mon, 4 Sep 2017 14:37:08 +0200 Subject: [PATCH 18/21] Add fallback for save feature, if auto generating ID fails --- Component/Drivers/DoctrineBaseDriver.php | 13 +++++++++++++ Component/FeatureType.php | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Component/Drivers/DoctrineBaseDriver.php b/Component/Drivers/DoctrineBaseDriver.php index a4b9dec..f5fffcc 100644 --- a/Component/Drivers/DoctrineBaseDriver.php +++ b/Component/Drivers/DoctrineBaseDriver.php @@ -467,4 +467,17 @@ protected function extractTypeValues(array $data, array $types) return $typeValues; } + + /** + * Return next possible ID + * + * @return mixed + */ + public function getNextPossibleId() + { + $con = $this->connection; + return $con->fetchColumn('SELECT MAX(' + . $con->quoteIdentifier($this->getUniqueId()) + . ")+1 FROM " . $con->quoteIdentifier($this->getTableName())); + } } \ No newline at end of file diff --git a/Component/FeatureType.php b/Component/FeatureType.php index e748ef7..c1d9221 100644 --- a/Component/FeatureType.php +++ b/Component/FeatureType.php @@ -6,6 +6,7 @@ use Doctrine\ORM\Mapping as ORM; use Mapbender\CoreBundle\Component\Application as AppComponent; use Mapbender\DataSourceBundle\Component\Drivers\BaseDriver; +use Mapbender\DataSourceBundle\Component\Drivers\DoctrineBaseDriver; use Mapbender\DataSourceBundle\Component\Drivers\Interfaces\Geographic; use Mapbender\DataSourceBundle\Component\Drivers\Oracle; use Mapbender\DataSourceBundle\Component\Drivers\PostgreSQL; @@ -191,7 +192,17 @@ public function save($featureData, $autoUpdate = true) if ($this->allowSave) { // Insert if no ID given if (!$autoUpdate || !$feature->hasId()) { - $feature = $this->insert($feature); + try{ + $feature = $this->insert($feature); + } catch (\Exception $e){ + // Fallback, if can't save, course no auto ID set + // then try it to set it manuel and try to save it one more time. + $driver = $this->getDriver(); + if($driver instanceof DoctrineBaseDriver){ + $feature->setId($driver->getNextPossibleId()); + $feature = $this->insert($feature); + } + } } // Replace if has ID else { $feature = $this->update($feature); @@ -313,6 +324,8 @@ public function update($featureData) * * @param array $criteria * @return Feature[] + * @throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException + * @throws \Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException */ public function search(array $criteria = array()) { From a842ae437aab751d1e52e3ba022013518dc558dc Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Thu, 7 Sep 2017 10:04:12 +0200 Subject: [PATCH 19/21] Optimize import of DataStore.php --- Component/DataStore.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/Component/DataStore.php b/Component/DataStore.php index 121c931..5c64f56 100644 --- a/Component/DataStore.php +++ b/Component/DataStore.php @@ -3,7 +3,6 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Statement; -use FOM\UserBundle\Entity\User; use Mapbender\DataSourceBundle\Component\Drivers\BaseDriver; use Mapbender\DataSourceBundle\Component\Drivers\DoctrineBaseDriver; use Mapbender\DataSourceBundle\Component\Drivers\Interfaces\Base; @@ -12,7 +11,6 @@ use Mapbender\DataSourceBundle\Component\Drivers\YAML; use Mapbender\DataSourceBundle\Entity\DataItem; use Symfony\Component\Config\Definition\Exception\Exception; -use Symfony\Component\DependencyInjection\ContainerAware; use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\DependencyInjection\ContainerInterface; From fb5c5411d307e5c1397cad2febd04a5dd8e0bdf5 Mon Sep 17 00:00:00 2001 From: wg-rolf Date: Thu, 28 Sep 2017 18:22:57 +0200 Subject: [PATCH 20/21] Set appropriate Content-Type header for JSON-serialized response --- Element/BaseElement.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Element/BaseElement.php b/Element/BaseElement.php index 92927e8..2f4ece4 100644 --- a/Element/BaseElement.php +++ b/Element/BaseElement.php @@ -56,7 +56,8 @@ public function httpAction($action) if (is_array($result)) { $serializer = new JsonSerializer(); - $result = new Response($serializer->serialize($result)); + $responseBody = $serializer->serialize($result); + $result = new Response($responseBody, 200, array('Content-Type' => 'application/json')); } return $result; @@ -179,4 +180,4 @@ public function decodeRequest(array $request) } return $request; } -} \ No newline at end of file +} From 54fffea2edee4657c698dfec8e7993aeb1493b4a Mon Sep 17 00:00:00 2001 From: Andriy Oblivantsev Date: Wed, 13 Dec 2017 21:42:25 +0100 Subject: [PATCH 21/21] Fix decode multipart request content --- Element/BaseElement.php | 82 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 6 deletions(-) diff --git a/Element/BaseElement.php b/Element/BaseElement.php index 2f4ece4..b22618d 100644 --- a/Element/BaseElement.php +++ b/Element/BaseElement.php @@ -3,7 +3,6 @@ use Doctrine\DBAL\Connection; use Mapbender\CoreBundle\Element\HTMLElement; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Zumba\Util\JsonSerializer; @@ -127,6 +126,71 @@ protected function prepareItem($item) return $item; } + /** + * + * Parse raw HTTP request data + * + * Pass in $a_data as an array. This is done by reference to avoid copying + * the data around too much. + * + * Any files found in the request will be added by their field name to the + * $data['files'] array. + * + * @see http://www.chlab.ch/blog/archives/webdevelopment/manually-parse-raw-http-data-php + * @param array Empty array to fill with data + * @return array Associative array of request data + */ + public static function parseMultiPartRequest($content) + { + $result = array(); + // read incoming data + + // grab multipart boundary from content type header + preg_match('/boundary=(.*)$/', $_SERVER['CONTENT_TYPE'], $matches); + + // content type is probably regular form-encoded + if (!count($matches)) { + // we expect regular puts to containt a query string containing data + parse_str(urldecode($content), $result); + return $result; + } + + $boundary = $matches[1]; + + // split content by boundary and get rid of last -- element + $a_blocks = preg_split("/-+$boundary/", $content); + array_pop($a_blocks); + + // loop data blocks + foreach ($a_blocks as $id => $block) { + if (empty($block)) { + continue; + } + + // you'll have to var_dump $block to understand this and maybe replace \n or \r with a visibile char + + // parse uploaded files + if (strpos($block, 'application/octet-stream') !== false) { + // match "name", then everything after "stream" (optional) except for prepending newlines + preg_match("/name=\"([^\"]*)\".*stream[\n|\r]+([^\n\r].*)?$/s", $block, $matches); + $keyName = preg_replace('/\[\]$/', '', $matches[1]); + + if (!isset($result[ $keyName ])) { + $result[ $keyName ] = array(); + } + $result[ $keyName ][] = $matches[2]; + + } // parse all other fields + else { + // match "name" and optional value in between newline sequences + preg_match('/name=\"([^\"]*)\"[\n|\r]+([^\n\r].*)?\r$/s', $block, $matches); + $result[ $matches[1] ] = $matches[2]; + } + } + + return $result; + } + /** * @return array|mixed * @throws \LogicException @@ -135,11 +199,17 @@ protected function prepareItem($item) */ protected function getRequestData() { - $content = $this->container->get('request')->getContent(); - $request = array_merge($_POST, $_GET); - - if (!empty($content)) { - $request = array_merge($request, json_decode($content, true)); + $content = $this->container->get('request')->getContent(); + $request = array_merge($_POST, $_GET); + $hasContent = !empty($content); + + if ($hasContent) { + $isMultipart = strpos($content, '-') === 0; + if ($isMultipart) { + $request = array_merge($request, static::parseMultiPartRequest($content)); + } else { + $request = array_merge($request, json_decode($content, true)); + } } return $this->decodeRequest($request);