Laravel model for Place that can have aliases (=duplicate entries) - how to treat them as one

378 views Asked by At

The GPS data I import can have multiple waypoint places for the same place.
I store this GPS data in the database, but want to treat all duplicates as one.

My Laravel (v6) model Place has id and parent_id (I could change this if necessary). E.g.

id parent_id name
1  null      real place
2  1         same as real place

When querying for Place info, I would always like to do this for the real place and the duplicates. So far I am using a local scope:

    public function scopeId($query, $id) {
        return $query->where('id', $id)->orWhere('parent_id', $id);
    }

This allows me to do e.g. Place::id(1)->with('visits')->get() which gives me e.g.

place 1
 - visit A
 - visit B
place 2
 - visit C

Is there a way to "merge" these Places so that the responses always return just the parent Place? Like:

place 1
 - visit A
 - visit B
 - visit C

The with('visits') is an example. Would like this functionality to apply to all Place related queries.

Edit: full example

Place::id(1078)->select('id','parent_id')->with('visits:id,place_id')->get()
=> Illuminate\Database\Eloquent\Collection {#3092
     all: [
       App\Place {#3115
         id: 1068,
         parent_id: 1078,
         visits: Illuminate\Database\Eloquent\Collection {#3109
           all: [
             App\Timeline {#3087
               id: 8022,
               place_id: 1068,
             },
             App\Timeline {#3094
               id: 8023,
               place_id: 1068,
             },
           ],
         },
       },
       App\Place {#3137
         id: 1078,
         parent_id: null,
         visits: Illuminate\Database\Eloquent\Collection {#3139
           all: [
             App\Timeline {#3117
               id: 8304,
               place_id: 1078,
             },
             App\Timeline {#3084
               id: 8401,
               place_id: 1078,
             },
             App\Timeline {#3116
               id: 8513,
               place_id: 1078,
             },
             App\Timeline {#3119
               id: 9363,
               place_id: 1078,
             },
           ],
         },
       },
     ],
   }
1

There are 1 answers

3
Sapnesh Naik On BEST ANSWER
  1. You can override the get() method from Eloquent by creating a new class CustomQueryBuilder that extends the Illuminate\Database\Query\Builder:

    Your customized get() method in CustomQueryBuilder:

    <?php
     namespace App\Override;
    
    class CustomQueryBuilder extends \Illuminate\Database\Query\Builder {
    
        private $needVisitsCombined = false;
    
        public function needVisitsCombined() {
                $this->needVisitsCombined = true;
                return $this;
        }
    
        //@Override
        public function get($columns = ['*']) {
            //Get the raw query string with the PDO bindings
            $originalResult = parent::get($columns);
    
            if($this->needVisitsCombined == false){
                return $originalResult;
            }
    
            $parentRecord = $originalResult->filter(function ($result, $key) {
                return $result->parent_id != null;
            });
    
            $originalResult->each(function ($result, $key) {
                if($result->parent_id == null) {
                    $parentRecord->visits()->concat($result->visits())
                }
            });
    
            return $parentRecord;
    
        }
    } 
    ?>
    
  2. Create a new class CustomModel that extends the Illuminate\Database\Eloquent\Model and there override the newBaseQueryBuilder like this:

    <?php
    namespace App\Override;
    
    use App\Override\CustomQueryBuilder;
    
    class CustomModel extends Illuminate\Database\Eloquent\Model {
    
        //@Override
        protected function newBaseQueryBuilder()
        {
            $connection = $this->getConnection();
    
            return new CustomQueryBuilder(
                $connection, $connection->getQueryGrammar(), $connection>getPostProcessor(), $this
            );
       }
    }
    ?>
    
  3. Finally, Now your Place Model ( or Whichever require this custom get() behavior) can extend CustomModel instead.

  4. Now your query will be something like:

    Place::id(1)->needVisitsCombined()->with('visits')->get() 
    

Note:

Be aware that all queries made from Models extending your CustomModel will get this new behavior, hence I have added a check with needVisitsCombined() method.

So if you're doing more customization do all the needed checks to don't mess up with Eloquent normal behaviour, something like this