返回

Laravel 数据转 GeoJSON 格式及错误处理

php

Laravel 数据转 GeoJSON: 解决 "Input data is not a valid GeoJSON object" 错误

开发 Laravel 项目,想把数据库里的评论数据转成 GeoJSON 格式,然后在 MapboxGL JS 里用。目前的问题是,用了网上的代码,Mapbox 报了个错:"Input data is not a valid GeoJSON object." 数据通过 GeoJSONLint 检测,提示 "old-style crs member is not recommended" 并且 geometry 类型不对,应该是对象,但现在是字符串。

问题原因分析

错误的核心在于,从数据库取出的 location 字段是 WKB (Well-Known Binary) 格式的,直接放到 GeoJSON 里,Mapbox 不认。 GeoJSON 需要的是一个几何形状的对象,包含 typecoordinates 属性,比如:

{
  "type": "Point",
  "coordinates": [125.6, 10.1]
}

另外,crs 属性虽然不是必需的,但在老版本的 GeoJSON 规范里有,新版已经不用了。最好去掉,免得引起混淆。

解决方案

有几个办法可以搞定这个问题。

方案一:用 PHP 直接处理 WKB

既然知道 location 字段是 WKB,那就直接在 PHP 里解析,转成 GeoJSON 格式。

  1. 安装 GeoPHP 库:

    这个库能处理各种地理空间数据格式。
    通过 composer 安装:

    composer require mjaschen/phpgeo
    
  2. 修改 CommentController 代码:

    用 GeoPHP 解析 WKB,然后构建 GeoJSON。

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\Comment;
    use geoPHP;
    
    class CommentController extends Controller
    {
        public function all()
        {
            $comments = Comment::whereNotNull('user_id')->get();
    
            $features = [];
            foreach ($comments as $comment) {
                // 解析 WKB
                $geometry = geoPHP::load($comment->location, 'wkb');
    
                $features[] = [
                    'type' => 'Feature',
                    'properties' => [
                        'id' => $comment->id,
                        // 可以把评论的其他字段也放这里
                        'body' => $comment->body
                    ],
                    'geometry' => $geometry->out('json') // 直接转成 GeoJSON
                ];
            }
    
            $featureCollection = [
                'type' => 'FeatureCollection',
                'features' => $features,
            ];
    
            return response()->json($featureCollection); // 用 Laravel 的 response()->json 方法更简洁
        }
    }
    
    

    进阶使用技巧
    如果Comment 表的数据很多,这种全部查询出数据后再解析 WKB 的方案性能可能存在问题,考虑把 wkb 的转换放到 sql 语句中,使用数据库的 geometry 函数进行查询和转换

    public function all()
        {
    
          $comments = Comment::select(
            'id',
            'body',
            DB::raw('ST_AsGeoJSON(location) as geojson')
        )
          ->whereNotNull('user_id')
            ->get();
    
            $features = [];
            foreach ($comments as $comment) {
                $features[] = [
                    'type' => 'Feature',
                    'properties' => [
                        'id' => $comment->id,
                        // 可以把评论的其他字段也放这里
                        'body' => $comment->body
                    ],
                    'geometry' => json_decode($comment->geojson)
                ];
            }
    
            $featureCollection = [
                'type' => 'FeatureCollection',
                'features' => $features,
            ];
    
            return response()->json($featureCollection); // 用 Laravel 的 response()->json 方法更简洁
        }
    
    

这种做法直接在数据库查询时转换好了, 会大大节省 PHP 的解析时间和内存。

方案二: 在 Model 里自定义属性

这个方法更优雅点,把转换逻辑封装到 Comment 模型里。

  1. 还是得装 GeoPHP:

    composer require mjaschen/phpgeo
    
  2. 修改 Comment 模型:

    加个 getGeojsonAttribute 方法,访问 $comment->geojson 时,自动转换。

    <?php
    
    namespace App\Models;
    
    use Illuminate\Database\Eloquent\Factories\HasFactory;
    use Illuminate\Database\Eloquent\Model;
    use geoPHP;
    
    class Comment extends Model
    {
        use HasFactory;
    
        protected $table = "comments"; //明确 table
    
        // 加个自定义属性
        public function getGeojsonAttribute()
        {
            return geoPHP::load($this->location, 'wkb')->out('json');
        }
    
        // 不想在 JSON 里显示 location 字段,可以隐藏掉
        protected $hidden = ['location'];
    
         // 如果要包含 created_at 和 updated_at 字段,但又不想直接输出,而是要格式化
         protected $casts = [
              'created_at' => 'datetime:Y-m-d H:i:s',
              'updated_at' => 'datetime:Y-m-d H:i:s',
            ];
    
         //如果需要增加虚拟字段
          protected $appends = ['geojson'];
    }
    
    
  3. 修改 CommentController

    现在直接取 $comment->geojson 就行了。

    <?php
    
    namespace App\Http\Controllers;
    
    use App\Models\Comment;
    
    class CommentController extends Controller
    {
        public function all()
        {
            $comments = Comment::whereNotNull('user_id')->get();
    
            $features = [];
            foreach ($comments as $comment) {
                $features[] = [
                    'type' => 'Feature',
                    'properties' => [
                        'id' => $comment->id,
                        'body' => $comment->body
                    ],
                    'geometry' => $comment->geojson, // 直接用模型的自定义属性
                ];
            }
    
            $featureCollection = [
                'type' => 'FeatureCollection',
                'features' => $features,
            ];
    
            return response()->json($featureCollection);
        }
    }
    
    

方案三: 使用数据库的 Geometry 函数 (推荐)

如果能直接改数据库查询语句,用数据库自带的函数转格式,效率最高。不同的数据库,函数名可能不一样。

  1. MySQL:

    ST_AsGeoJSON 函数。

    <?php
      namespace App\Http\Controllers;
    
    use App\Models\Comment;
    use Illuminate\Support\Facades\DB; //记得加
    
    class CommentController extends Controller{
    
      public function all(){
        //直接在查询时转换
        $comments = Comment::select(
          'id',
          'body',
          DB::raw('ST_AsGeoJSON(location) as geojson')
        )
      ->whereNotNull('user_id')
        ->get();
    
          $features = [];
    
        foreach($comments as $comment){
          $features[] = [
            'type' => 'Feature',
            'properties' =>[
              'id' => $comment->id,
              'body' => $comment ->body
            ],
             'geometry' => json_decode($comment->geojson) //这里需要 json_decode 一下
          ];
        }
    
        $featureCollection = [
            'type' => 'FeatureCollection',
            'features' => $features,
          ];
    
        return  response()->json($featureCollection);
      }
    }
    
  2. PostgreSQL / PostGIS:

    ST_AsGeoJSON

     // 和 MySQL 版本的几乎一样,就不重复写了.
    

如果数据量很大, 还是在 SQL 查询中完成转换,这样最有效率.

安全建议

  • 输入验证: 不管用哪种方法,如果评论里有用户输入的内容,记得验证和过滤,防止 XSS 之类的攻击。
  • 数据库安全: 确保数据库连接安全,防止 SQL 注入。

这几个方案,选哪个看具体情况。方案三,直接在数据库里转,通常是效率最高的。如果改不了数据库,那就用方案一或二,把转换逻辑放到 PHP 里。方案二更“面向对象”一些,把转换逻辑封装在模型里,代码更清晰。