Add importers in console and admin for flights/aircraft/subfleets and airport #194

This commit is contained in:
Nabeel Shahzad
2018-03-20 19:17:11 -05:00
parent 782121829a
commit b9beb6c804
41 changed files with 1270 additions and 225 deletions

View File

@@ -0,0 +1,59 @@
<?php
/**
*
*/
namespace App\Console\Commands;
use App\Console\Command;
use App\Services\ImporterService;
/**
* Class ImportCsv
* @package App\Console\Commands
*/
class ImportCsv extends Command
{
protected $signature = 'phpvms:csv-import {type} {file}';
protected $description = 'Import from a CSV file';
private $importer;
/**
* Import constructor.
* @param ImporterService $importer
*/
public function __construct(ImporterService $importer)
{
parent::__construct();
$this->importer = $importer;
}
/**
* @return mixed|void
* @throws \League\Csv\Exception
*/
public function handle()
{
$type = $this->argument('type');
$file = $this->argument('file');
if (\in_array($type, ['flight', 'flights'])) {
$status = $this->importer->importFlights($file);
} elseif ($type === 'aircraft') {
$status = $this->importer->importAircraft($file);
} elseif (\in_array($type, ['airport', 'airports'])) {
$status = $this->importer->importAirports($file);
} elseif ($type === 'subfleet') {
$status = $this->importer->importSubfleets($file);
}
foreach($status['success'] as $line) {
$this->info($line);
}
foreach ($status['failed'] as $line) {
$this->error($line);
}
}
}

View File

@@ -5,8 +5,8 @@ use Faker\Generator as Faker;
$factory->define(App\Models\Fare::class, function (Faker $faker) {
return [
'id' => null,
'code' => $faker->text(5),
'name' => $faker->text(20),
'code' => $faker->unique()->text(50),
'name' => $faker->text(50),
'price' => $faker->randomFloat(2, 100, 1000),
'cost' => function (array $fare) {
return round($fare['price'] / 2);

View File

@@ -14,7 +14,7 @@ class CreateFaresTable extends Migration
{
Schema::create('fares', function (Blueprint $table) {
$table->increments('id');
$table->string('code', 50);
$table->string('code', 50)->unique();
$table->string('name', 50);
$table->unsignedDecimal('price')->nullable()->default(0.00);
$table->unsignedDecimal('cost')->nullable()->default(0.00);

View File

@@ -13,8 +13,8 @@ class CreateSubfleetTables extends Migration
Schema::create('subfleets', function (Blueprint $table) {
$table->increments('id');
$table->unsignedInteger('airline_id')->nullable();
$table->string('type', 50)->unique();
$table->string('name', 50);
$table->string('type', 50);
$table->unsignedTinyInteger('fuel_type')->nullable();
$table->unsignedDecimal('ground_handling_multiplier')->nullable()->default(100);
$table->unsignedDecimal('cargo_capacity')->nullable();

View File

@@ -10,9 +10,12 @@ use App\Models\Enums\AircraftStatus;
use App\Models\Expense;
use App\Models\Subfleet;
use App\Repositories\AircraftRepository;
use App\Services\ImporterService;
use Flash;
use Illuminate\Http\Request;
use Log;
use Prettus\Repository\Criteria\RequestCriteria;
use Storage;
/**
* Class AircraftController
@@ -20,16 +23,20 @@ use Prettus\Repository\Criteria\RequestCriteria;
*/
class AircraftController extends Controller
{
private $aircraftRepo;
private $aircraftRepo,
$importSvc;
/**
* AircraftController constructor.
* @param AircraftRepository $aircraftRepo
* @param ImporterService $importSvc
*/
public function __construct(
AircraftRepository $aircraftRepo
AircraftRepository $aircraftRepo,
ImporterService $importSvc
) {
$this->aircraftRepo = $aircraftRepo;
$this->importSvc = $importSvc;
}
/**
@@ -67,7 +74,6 @@ class AircraftController extends Controller
$aircraft = $this->aircraftRepo->create($attrs);
Flash::success('Aircraft saved successfully.');
return redirect(route('admin.aircraft.edit', ['id' => $aircraft->id]));
}
@@ -80,7 +86,6 @@ class AircraftController extends Controller
if (empty($aircraft)) {
Flash::error('Aircraft not found');
return redirect(route('admin.aircraft.index'));
}
@@ -119,7 +124,6 @@ class AircraftController extends Controller
if (empty($aircraft)) {
Flash::error('Aircraft not found');
return redirect(route('admin.aircraft.index'));
}
@@ -127,7 +131,6 @@ class AircraftController extends Controller
$this->aircraftRepo->update($attrs, $id);
Flash::success('Aircraft updated successfully.');
return redirect(route('admin.aircraft.index'));
}
@@ -140,17 +143,43 @@ class AircraftController extends Controller
if (empty($aircraft)) {
Flash::error('Aircraft not found');
return redirect(route('admin.aircraft.index'));
}
$this->aircraftRepo->delete($id);
Flash::success('Aircraft deleted successfully.');
return redirect(route('admin.aircraft.index'));
}
/**
*
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \League\Csv\Exception
*/
public function import(Request $request)
{
$logs = [
'success' => [],
'failed' => [],
];
if ($request->isMethod('post')) {
$path = Storage::putFileAs(
'import', $request->file('csv_file'), 'aircraft'
);
$path = storage_path('app/'.$path);
Log::info('Uploaded flights import file to '.$path);
$logs = $this->importSvc->importAircraft($path);
}
return view('admin.aircraft.import', [
'logs' => $logs,
]);
}
/**
* @param Aircraft|null $aircraft
* @return mixed

View File

@@ -9,10 +9,13 @@ use App\Models\Airport;
use App\Models\Expense;
use App\Repositories\AirportRepository;
use App\Repositories\Criteria\WhereCriteria;
use App\Services\ImporterService;
use Flash;
use Illuminate\Http\Request;
use Jackiedo\Timezonelist\Facades\Timezonelist;
use Log;
use Response;
use Storage;
/**
* Class AirportController
@@ -20,15 +23,19 @@ use Response;
*/
class AirportController extends Controller
{
private $airportRepo;
private $airportRepo,
$importSvc;
/**
* @param AirportRepository $airportRepo
* @param ImporterService $importSvc
*/
public function __construct(
AirportRepository $airportRepo
AirportRepository $airportRepo,
ImporterService $importSvc
) {
$this->airportRepo = $airportRepo;
$this->importSvc = $importSvc;
}
/**
@@ -162,17 +169,43 @@ class AirportController extends Controller
if (empty($airport)) {
Flash::error('Airport not found');
return redirect(route('admin.airports.index'));
}
$this->airportRepo->delete($id);
Flash::success('Airport deleted successfully.');
return redirect(route('admin.airports.index'));
}
/**
*
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \League\Csv\Exception
*/
public function import(Request $request)
{
$logs = [
'success' => [],
'failed' => [],
];
if ($request->isMethod('post')) {
$path = Storage::putFileAs(
'import', $request->file('csv_file'), 'airports'
);
$path = storage_path('app/'.$path);
Log::info('Uploaded flights import file to '.$path);
$logs = $this->importSvc->importAirports($path);
}
return view('admin.airports.import', [
'logs' => $logs,
]);
}
/**
* @param Airport|null $airport
* @return mixed

View File

@@ -17,11 +17,13 @@ use App\Repositories\FlightRepository;
use App\Repositories\SubfleetRepository;
use App\Services\FareService;
use App\Services\FlightService;
use App\Services\ImporterService;
use App\Support\Units\Time;
use Flash;
use Illuminate\Http\Request;
use Log;
use Response;
use Storage;
/**
* Class FlightController
@@ -36,6 +38,7 @@ class FlightController extends Controller
$flightFieldRepo,
$fareSvc,
$flightSvc,
$importSvc,
$subfleetRepo;
/**
@@ -44,9 +47,10 @@ class FlightController extends Controller
* @param AirportRepository $airportRepo
* @param FareRepository $fareRepo
* @param FlightRepository $flightRepo
* @param FlightFieldRepository $flightFieldRepository
* @param FlightFieldRepository $flightFieldRepo
* @param FareService $fareSvc
* @param FlightService $flightSvc
* @param ImporterService $importSvc
* @param SubfleetRepository $subfleetRepo
*/
public function __construct(
@@ -57,6 +61,7 @@ class FlightController extends Controller
FlightFieldRepository $flightFieldRepo,
FareService $fareSvc,
FlightService $flightSvc,
ImporterService $importSvc,
SubfleetRepository $subfleetRepo
) {
$this->airlineRepo = $airlineRepo;
@@ -66,6 +71,7 @@ class FlightController extends Controller
$this->flightFieldRepo = $flightFieldRepo;
$this->fareSvc = $fareSvc;
$this->flightSvc = $flightSvc;
$this->importSvc = $importSvc;
$this->subfleetRepo = $subfleetRepo;
}
@@ -304,6 +310,34 @@ class FlightController extends Controller
return redirect(route('admin.flights.index'));
}
/**
*
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \League\Csv\Exception
*/
public function import(Request $request)
{
$logs = [
'success' => [],
'failed' => [],
];
if ($request->isMethod('post')) {
$path = Storage::putFileAs(
'import', $request->file('csv_file'), 'flights'
);
$path = storage_path('app/'.$path);
Log::info('Uploaded flights import file to '.$path);
$logs = $this->importSvc->importFlights($path);
}
return view('admin.flights.import', [
'logs' => $logs,
]);
}
/**
* @param $flight
* @return mixed

View File

@@ -15,10 +15,13 @@ use App\Repositories\RankRepository;
use App\Repositories\SubfleetRepository;
use App\Services\FareService;
use App\Services\FleetService;
use App\Services\ImporterService;
use Flash;
use Illuminate\Http\Request;
use Log;
use Prettus\Repository\Criteria\RequestCriteria;
use Response;
use Storage;
/**
* Class SubfleetController
@@ -30,6 +33,7 @@ class SubfleetController extends Controller
$fareRepo,
$fareSvc,
$fleetSvc,
$importSvc,
$rankRepo,
$subfleetRepo;
@@ -37,23 +41,26 @@ class SubfleetController extends Controller
* SubfleetController constructor.
* @param AircraftRepository $aircraftRepo
* @param FleetService $fleetSvc
* @param RankRepository $rankRepo
* @param SubfleetRepository $subfleetRepo
* @param FareRepository $fareRepo
* @param FareService $fareSvc
* @param ImporterService $importSvc
* @param RankRepository $rankRepo
* @param SubfleetRepository $subfleetRepo
*/
public function __construct(
AircraftRepository $aircraftRepo,
FleetService $fleetSvc,
RankRepository $rankRepo,
SubfleetRepository $subfleetRepo,
FareRepository $fareRepo,
FareService $fareSvc
FareService $fareSvc,
ImporterService $importSvc,
RankRepository $rankRepo,
SubfleetRepository $subfleetRepo
) {
$this->aircraftRepo = $aircraftRepo;
$this->fareRepo = $fareRepo;
$this->fareSvc = $fareSvc;
$this->fleetSvc = $fleetSvc;
$this->importSvc = $importSvc;
$this->rankRepo = $rankRepo;
$this->subfleetRepo = $subfleetRepo;
}
@@ -133,7 +140,6 @@ class SubfleetController extends Controller
$subfleet = $this->subfleetRepo->create($input);
Flash::success('Subfleet saved successfully.');
return redirect(route('admin.subfleets.edit', ['id' => $subfleet->id]));
}
@@ -148,12 +154,10 @@ class SubfleetController extends Controller
if (empty($subfleet)) {
Flash::error('Subfleet not found');
return redirect(route('admin.subfleets.index'));
}
$avail_fares = $this->getAvailFares($subfleet);
return view('admin.subfleets.show', [
'subfleet' => $subfleet,
'avail_fares' => $avail_fares,
@@ -171,7 +175,6 @@ class SubfleetController extends Controller
if (empty($subfleet)) {
Flash::error('Subfleet not found');
return redirect(route('admin.subfleets.index'));
}
@@ -200,14 +203,12 @@ class SubfleetController extends Controller
if (empty($subfleet)) {
Flash::error('Subfleet not found');
return redirect(route('admin.subfleets.index'));
}
$this->subfleetRepo->update($request->all(), $id);
Flash::success('Subfleet updated successfully.');
return redirect(route('admin.subfleets.index'));
}
@@ -222,7 +223,6 @@ class SubfleetController extends Controller
if (empty($subfleet)) {
Flash::error('Subfleet not found');
return redirect(route('admin.subfleets.index'));
}
@@ -231,17 +231,43 @@ class SubfleetController extends Controller
$aircraft = $this->aircraftRepo->findWhere(['subfleet_id' => $id], ['id']);
if ($aircraft->count() > 0) {
Flash::error('There are aircraft still assigned to this subfleet, you can\'t delete it!')->important();
return redirect(route('admin.subfleets.index'));
}
$this->subfleetRepo->delete($id);
Flash::success('Subfleet deleted successfully.');
return redirect(route('admin.subfleets.index'));
}
/**
*
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \League\Csv\Exception
*/
public function import(Request $request)
{
$logs = [
'success' => [],
'failed' => [],
];
if ($request->isMethod('post')) {
$path = Storage::putFileAs(
'import', $request->file('csv_file'), 'subfleets'
);
$path = storage_path('app/'.$path);
Log::info('Uploaded flights import file to '.$path);
$logs = $this->importSvc->importSubfleets($path);
}
return view('admin.subfleets.import', [
'logs' => $logs,
]);
}
/**
* @param Subfleet $subfleet
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
@@ -251,7 +277,6 @@ class SubfleetController extends Controller
$subfleet->refresh();
$avail_ranks = $this->getAvailRanks($subfleet);
return view('admin.subfleets.ranks', [
'subfleet' => $subfleet,
'avail_ranks' => $avail_ranks,
@@ -267,7 +292,6 @@ class SubfleetController extends Controller
$subfleet->refresh();
$avail_fares = $this->getAvailFares($subfleet);
return view('admin.subfleets.fares', [
'subfleet' => $subfleet,
'avail_fares' => $avail_fares,
@@ -321,7 +345,6 @@ class SubfleetController extends Controller
protected function return_expenses_view(?Subfleet $subfleet)
{
$subfleet->refresh();
return view('admin.subfleets.expenses', [
'subfleet' => $subfleet,
]);
@@ -377,7 +400,6 @@ class SubfleetController extends Controller
$subfleet = $this->subfleetRepo->findWithoutFail($id);
if (empty($subfleet)) {
return $this->return_fares_view($subfleet);
//return view('admin.aircraft.fares', ['fares' => []]);
}
if ($request->isMethod('get')) {

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Interfaces;
use App\Models\Airline;
/**
* Common functionality used across all of the importers
* @package App\Interfaces
*/
class ImportExport
{
public $status;
/**
* Hold the columns for the particular table
*/
public static $columns = [];
/**
* Need to implement in a child class!
* @throws \RuntimeException
*/
public function export()
{
throw new \RuntimeException('Calling export, needs to be implemented in child!');
}
/**
* Need to implement in a child class!
* @param array $row
* @param $index
* @throws \RuntimeException
*/
public function import(array $row, $index)
{
throw new \RuntimeException('Calling import, needs to be implemented in child!');
}
/**
* Get the airline from the ICAO
* @param $code
* @return \App\Models\Airline
*/
public function getAirline($code)
{
return Airline::where('icao', $code)->first();
}
/**
* @return array
*/
public function getColumns()
{
return static::$columns;
}
/**
* Set a key-value pair to an array
* @param $kvp_str
* @param array $arr
*/
protected function kvpToArray($kvp_str, array &$arr)
{
$item = explode('=', $kvp_str);
if (\count($item) === 1) { # just a list?
$arr[] = trim($item[0]);
} else { # actually a key-value pair
$k = trim($item[0]);
$v = trim($item[1]);
$arr[$k] = $v;
}
}
/**
* Parse a multi column values field. E.g:
* Y?price=200&cost=100; F?price=1200
* or
* gate=B32;cost index=100
*
* Converted into a multi-dimensional array
*
* @param $field
* @return array|string
*/
public function parseMultiColumnValues($field)
{
$ret = [];
$split_values = explode(';', $field);
# No multiple values in here, just a straight value
if (\count($split_values) === 1) {
return [$split_values[0]];
}
foreach ($split_values as $value) {
# This isn't in the query string format, so it's
# just a straight key-value pair set
if (strpos($value, '?') === false) {
$this->kvpToArray($value, $ret);
continue;
}
# This contains the query string, which turns it
# into the multi-level array
$query_str = explode('?', $value);
$parent = trim($query_str[0]);
$children = [];
$kvp = explode('&', trim($query_str[1]));
foreach ($kvp as $items) {
$this->kvpToArray($items, $children);
}
$ret[$parent] = $children;
}
return $ret;
}
}

View File

@@ -7,6 +7,7 @@ use App\Models\Enums\AircraftStatus;
use App\Models\Traits\ExpensableTrait;
/**
* @property int id
* @property mixed subfleet_id
* @property string name
* @property string icao
@@ -38,8 +39,6 @@ class Aircraft extends Model
/**
* The attributes that should be casted to native types.
*
* @var array
*/
protected $casts = [
'subfleet_id' => 'integer',
@@ -50,8 +49,6 @@ class Aircraft extends Model
/**
* Validation rules
*
* @var array
*/
public static $rules = [
'subfleet_id' => 'required',

View File

@@ -8,6 +8,7 @@ use App\Models\Traits\JournalTrait;
/**
* Class Airline
* @property mixed id
* @property string code
* @property string icao
* @property string iata

View File

@@ -19,4 +19,31 @@ class FlightType extends Enum
FlightType::CARGO => 'Cargo',
FlightType::CHARTER => 'Charter',
];
/**
* Return value from P, C or H
* @param $code
* @return int
*/
public static function getFromCode($code): int
{
if(is_numeric($code)) {
return (int) $code;
}
$code = strtolower($code);
if($code === 'p') {
return self::PASSENGER;
}
if ($code === 'c') {
return self::CARGO;
}
if($code === 'h') {
return self::CHARTER;
}
return self::PASSENGER;
}
}

View File

@@ -33,7 +33,7 @@ class Fare extends Model
];
public static $rules = [
'code' => 'required',
'code' => 'required|unique',
'name' => 'required',
];

View File

@@ -21,8 +21,8 @@ class Subfleet extends Model
protected $fillable = [
'airline_id',
'name',
'type',
'name',
'fuel_type',
'ground_handling_multiplier',
'cargo_capacity',
@@ -40,8 +40,8 @@ class Subfleet extends Model
];
protected static $rules = [
'type' => 'required|unique',
'name' => 'required',
'type' => 'required',
'ground_handling_multiplier' => 'nullable|numeric',
];

View File

@@ -10,15 +10,17 @@ Route::group([
Route::resource('airlines', 'AirlinesController');
Route::match(['get', 'post', 'put'], 'airports/fuel', 'AirportController@fuel');
Route::resource('airports', 'AirportController');
Route::match(['get', 'post'], 'airports/import', 'AirportController@import')->name('airports.import');
Route::match(['get', 'post', 'put', 'delete'], 'airports/{id}/expenses', 'AirportController@expenses');
Route::resource('airports', 'AirportController');
# Awards
Route::resource('awards', 'AwardController');
# aircraft and fare associations
Route::resource('aircraft', 'AircraftController');
Route::match(['get', 'post'], 'aircraft/import', 'AircraftController@import')->name('aircraft.import');
Route::match(['get', 'post', 'put', 'delete'], 'aircraft/{id}/expenses', 'AircraftController@expenses');
Route::resource('aircraft', 'AircraftController');
# expenses
Route::resource('expenses', 'ExpenseController');
@@ -30,10 +32,11 @@ Route::group([
Route::resource('finances', 'FinanceController');
# flights and aircraft associations
Route::resource('flights', 'FlightController');
Route::match(['get', 'post'], 'flights/import', 'FlightController@import')->name('flights.import');
Route::match(['get', 'post', 'put', 'delete'], 'flights/{id}/fares', 'FlightController@fares');
Route::match(['get', 'post', 'put', 'delete'], 'flights/{id}/fields', 'FlightController@field_values');
Route::match(['get', 'post', 'put', 'delete'], 'flights/{id}/subfleets', 'FlightController@subfleets');
Route::resource('flights', 'FlightController');
Route::resource('flightfields', 'FlightFieldController');
@@ -55,10 +58,11 @@ Route::group([
Route::match(['post', 'put'], 'settings', 'SettingsController@update')->name('settings.update');
# subfleet
Route::resource('subfleets', 'SubfleetController');
Route::match(['get', 'post'], 'subfleets/import', 'SubfleetController@import')->name('subfleets.import');
Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/expenses', 'SubfleetController@expenses');
Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/fares', 'SubfleetController@fares');
Route::match(['get', 'post', 'put', 'delete'], 'subfleets/{id}/ranks', 'SubfleetController@ranks');
Route::resource('subfleets', 'SubfleetController');
Route::resource('users', 'UserController');
Route::get('users/{id}/regen_apikey',

View File

@@ -114,15 +114,15 @@ class FlightService extends Service
/**
* Update any custom PIREP fields
* @param Flight $flight_id
* @param array $field_values
* @param Flight $flight
* @param array $field_values
*/
public function updateCustomFields(Flight $flight_id, array $field_values): void
public function updateCustomFields(Flight $flight, array $field_values): void
{
foreach ($field_values as $fv) {
FlightFieldValue::updateOrCreate(
[
'flight_id' => $flight_id,
'flight_id' => $flight->id,
'name' => $fv['name'],
],
[

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Services\Import;
use App\Interfaces\ImportExport;
use App\Models\Aircraft;
use App\Models\Enums\AircraftState;
use App\Models\Enums\AircraftStatus;
use App\Models\Subfleet;
use App\Support\ICAO;
/**
* Import aircraft
* @package App\Services\Import
*/
class AircraftImporter extends ImportExport
{
/**
* All of the columns that are in the CSV import
* Should match the database fields, for the most part
*/
public static $columns = [
'subfleet',
'name',
'registration',
'hex_code',
'status',
];
/**
* Find the subfleet specified, or just create it on the fly
* @param $type
* @return Subfleet|\Illuminate\Database\Eloquent\Model|null|object|static
*/
protected function getSubfleet($type)
{
$subfleet = Subfleet::where(['type' => $type])->first();
if (!$subfleet) {
$subfleet = new Subfleet([
'type' => $type,
'name' => $type,
]);
$subfleet->save();
}
return $subfleet;
}
/**
* Import a flight, parse out the different rows
* @param array $row
* @param int $index
* @return bool
*/
public function import(array $row, $index)
{
$subfleet = $this->getSubfleet($row['subfleet']);
$row['subfleet_id'] = $subfleet->id;
# Generate a hex code
if(!$row['hex_code']) {
$row['hex_code'] = ICAO::createHexCode();
}
# Set a default status
if($row['status'] === null) {
$row['status'] = AircraftStatus::ACTIVE;
}
# Just set its state right now as parked
$row['state'] = AircraftState::PARKED;
# Try to add or update
$aircraft = Aircraft::firstOrNew([
'registration' => $row['registration'],
], $row);
try {
$aircraft->save();
} catch(\Exception $e) {
$this->status = 'Error in row '.$index.': '.$e->getMessage();
return false;
}
$this->status = 'Imported '.$row['registration'].' '.$row['name'];
return true;
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace App\Services\Import;
use App\Interfaces\ImportExport;
use App\Models\Airport;
/**
* Import airports
* @package App\Services\Import
*/
class AirportImporter extends ImportExport
{
/**
* All of the columns that are in the CSV import
* Should match the database fields, for the most part
*/
public static $columns = [
'iata',
'icao',
'name',
'location',
'country',
'timezone',
'hub',
'lat',
'lon',
];
/**
* Import a flight, parse out the different rows
* @param array $row
* @param int $index
* @return bool
*/
public function import(array $row, $index)
{
$row['id'] = $row['icao'];
$row['hub'] = get_truth_state($row['hub']);
$airport = Airport::firstOrNew([
'id' => $row['icao']
], $row);
try {
$airport->save();
} catch(\Exception $e) {
$this->status = 'Error in row '.$index.': '.$e->getMessage();
return false;
}
$this->status = 'Imported ' . $row['icao'];
return true;
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace App\Services\Import;
use App\Interfaces\ImportExport;
use App\Models\Enums\FlightType;
use App\Models\Fare;
use App\Models\Flight;
use App\Models\Subfleet;
use App\Repositories\AirlineRepository;
use App\Services\FareService;
use App\Services\FlightService;
use Log;
/**
* The flight importer can be imported or export. Operates on rows
*
* @package App\Services\Import
*/
class FlightImporter extends ImportExport
{
/**
* All of the columns that are in the CSV import
* Should match the database fields, for the most part
*/
public static $columns = [
'airline',
'flight_number',
'route_code',
'route_leg',
'dpt_airport_id',
'arr_airport_id',
'alt_airport_id',
'days',
'dpt_time',
'arr_time',
'level',
'distance',
'flight_time',
'flight_type',
'route',
'notes',
'active',
'subfleets',
'fares',
'fields',
];
/**
*
*/
private $airlineRepo,
$fareSvc,
$flightSvc;
/**
* FlightImportExporter constructor.
*/
public function __construct()
{
$this->airlineRepo = app(AirlineRepository::class);
$this->fareSvc = app(FareService::class);
$this->flightSvc = app(FlightService::class);
}
/**
* Import a flight, parse out the different rows
* @param array $row
* @param int $index
* @return bool
*/
public function import(array $row, $index)
{
// Get the airline ID from the ICAO code
$airline = $this->getAirline($row['airline']);
// Try to find this flight
$flight = Flight::firstOrNew([
'airline_id' => $airline->id,
'flight_number' => $row['flight_number'],
'route_code' => $row['route_code'],
'route_leg' => $row['route_leg'],
], $row);
// Any specific transformations
// Flight type can be set to P - Passenger, C - Cargo, or H - Charter
$flight->setAttribute('flight_type', FlightType::getFromCode($row['flight_type']));
$flight->setAttribute('active', get_truth_state($row['active']));
try {
$flight->save();
} catch (\Exception $e) {
$this->status = 'Error in row '.$index.': '.$e->getMessage();
return false;
}
$this->processSubfleets($flight, $row['subfleets']);
$this->processFares($flight, $row['fares']);
$this->processFields($flight, $row['fields']);
$this->status = 'Imported row '.$index;
return true;
}
/**
* Parse out all of the subfleets and associate them to the flight
* The subfleet is created if it doesn't exist
* @param Flight $flight
* @param $col
*/
protected function processSubfleets(Flight &$flight, $col): void
{
$count = 0;
$subfleets = $this->parseMultiColumnValues($col);
foreach($subfleets as $subfleet_type) {
$subfleet = Subfleet::firstOrNew(
['type' => $subfleet_type],
['name' => $subfleet_type]
);
$subfleet->save();
# sync
$flight->subfleets()->syncWithoutDetaching([$subfleet->id]);
$count ++;
}
Log::info('Subfleets added/processed: '.$count);
}
/**
* Parse all of the fares in the multi-format
* @param Flight $flight
* @param $col
*/
protected function processFares(Flight &$flight, $col): void
{
$fares = $this->parseMultiColumnValues($col);
foreach ($fares as $fare_code => $fare_attributes) {
if (\is_int($fare_code)) {
$fare_code = $fare_attributes;
$fare_attributes = [];
}
$fare = Fare::firstOrNew(['code' => $fare_code], ['name' => $fare_code]);
$this->fareSvc->setForFlight($flight, $fare, $fare_attributes);
}
}
/**
* Parse all of the subfields
* @param Flight $flight
* @param $col
*/
protected function processFields(Flight &$flight, $col): void
{
$pass_fields = [];
$fields = $this->parseMultiColumnValues($col);
foreach($fields as $field_name => $field_value) {
$pass_fields[] = [
'name' => $field_name,
'value' => $field_value,
];
}
$this->flightSvc->updateCustomFields($flight, $pass_fields);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Services\Import;
use App\Interfaces\ImportExport;
use App\Models\Subfleet;
/**
* Import subfleets
* @package App\Services\Import
*/
class SubfleetImporter extends ImportExport
{
/**
* All of the columns that are in the CSV import
* Should match the database fields, for the most part
*/
public static $columns = [
'airline',
'type',
'name',
];
/**
* Import a flight, parse out the different rows
* @param array $row
* @param int $index
* @return bool
*/
public function import(array $row, $index)
{
$airline = $this->getAirline($row['airline']);
if(!$airline) {
$this->status = 'Airline '.$row['airline'].' not found, row: '.$index;
return false;
}
$row['airline_id'] = $airline->id;
$subfleet = Subfleet::firstOrNew([
'type' => $row['type']
], $row);
try {
$subfleet->save();
} catch(\Exception $e) {
$this->status = 'Error in row '.$index.': '.$e->getMessage();
return false;
}
$this->status = 'Imported ' . $row['type'];
return true;
}
}

View File

@@ -2,8 +2,15 @@
namespace App\Services;
use App\Interfaces\ImportExport;
use App\Interfaces\Service;
use App\Models\Airport;
use App\Repositories\FlightRepository;
use App\Services\Import\AircraftImporter;
use App\Services\Import\AirportImporter;
use App\Services\Import\FlightImporter;
use App\Services\Import\SubfleetImporter;
use League\Csv\Reader;
/**
* Class ImporterService
@@ -24,75 +31,140 @@ class ImporterService extends Service
}
/**
* Set a key-value pair to an array
* @param $kvp_str
* @param array $arr
* @param $csv_file
* @return Reader
* @throws \League\Csv\Exception
*/
protected function setKvp($kvp_str, array &$arr)
public function openCsv($csv_file)
{
$item = explode('=', $kvp_str);
if (\count($item) === 1) { # just a list?
$arr[] = trim($item[0]);
} else { # actually a key-value pair
$k = trim($item[0]);
$v = trim($item[1]);
$arr[$k] = $v;
}
$reader = Reader::createFromPath($csv_file);
$reader->setDelimiter(',');
$reader->setEnclosure('"');
return $reader;
}
/**
* Parse a multi column values field. E.g:
* Y?price=200&cost=100; F?price=1200
* or
* gate=B32;cost index=100
*
* Converted into a multi-dimensional array
*
* @param $field
* @return array|string
* Run the actual importer
* @param Reader $reader
* @param ImportExport $importer
* @return array
*/
public function parseMultiColumnValues($field)
protected function runImport(Reader $reader, ImportExport $importer): array
{
$ret = [];
$split_values = explode(';', $field);
$import_report = [
'success' => [],
'failed' => [],
];
# No multiple values in here, just a straight value
if (\count($split_values) === 1) {
return $split_values[0];
}
$cols = $importer->getColumns();
$first_header = $cols[0];
foreach ($split_values as $value) {
# This isn't in the query string format, so it's
# just a straight key-value pair set
if (strpos($value, '?') === false) {
$this->setKvp($value, $ret);
$records = $reader->getRecords($cols);
foreach ($records as $offset => $row) {
// check if the first row being read is the header
if ($row[$first_header] === $first_header) {
continue;
}
# This contains the query string, which turns it
# into the multi-level array
$query_str = explode('?', $value);
$parent = trim($query_str[0]);
$children = [];
$kvp = explode('&', trim($query_str[1]));
foreach ($kvp as $items) {
$this->setKvp($items, $children);
$success = $importer->import($row, $offset);
if ($success) {
$import_report['success'][] = $importer->status;
} else {
$import_report['failed'][] = $importer->status;
}
$ret[$parent] = $children;
}
return $ret;
return $import_report;
}
/**
* Import aircraft
* @param string $csv_file
* @param bool $delete_previous
* @return mixed
* @throws \League\Csv\Exception
*/
public function importAircraft($csv_file, bool $delete_previous = true)
{
if ($delete_previous) {
# TODO: delete airports
}
$reader = $this->openCsv($csv_file);
if (!$reader) {
return false;
}
$importer = new AircraftImporter();
return $this->runImport($reader, $importer);
}
/**
* Import airports
* @param string $csv_file
* @param bool $delete_previous
* @return mixed
* @throws \League\Csv\Exception
*/
public function importAirports($csv_file, bool $delete_previous = true)
{
if ($delete_previous) {
Airport::truncate();
}
$reader = $this->openCsv($csv_file);
if (!$reader) {
return false;
}
$importer = new AirportImporter();
return $this->runImport($reader, $importer);
}
/**
* Import flights
* @param $csv_str
* @param bool $delete_previous
* @param string $csv_file
* @param bool $delete_previous
* @return mixed
* @throws \League\Csv\Exception
*/
public function importFlights($csv_str, bool $delete_previous = true)
public function importFlights($csv_file, bool $delete_previous = true)
{
if ($delete_previous) {
# TODO: Delete all from: flights, flight_field_values
}
$reader = $this->openCsv($csv_file);
if (!$reader) {
# TODO: Throw an error
return false;
}
$importer = new FlightImporter();
return $this->runImport($reader, $importer);
}
/**
* Import subfleets
* @param string $csv_file
* @param bool $delete_previous
* @return mixed
* @throws \League\Csv\Exception
*/
public function importSubfleets($csv_file, bool $delete_previous = true)
{
if ($delete_previous) {
# TODO: Cleanup subfleet data
}
$reader = $this->openCsv($csv_file);
if (!$reader) {
# TODO: Throw an error
return false;
}
$importer = new SubfleetImporter();
return $this->runImport($reader, $importer);
}
}