Refactoring Multi-Dimensional Array PHP to Object Oriented PHP

One of my favourite things to do is refactor legacy code. I get to see the thinking process that went into solving the problem. It’s like standing on the shoulders of giants.

private function convertActivityToArray(array $activity)
    {
        $activityArray = array();

        $previousGroupNumber = null;
        /** @var Activity $activityItem */
        foreach ($activity as $activityItem) {
            $type = $activityItem->getType();
            $groupNumber = $activityItem->getGroupNumber();
            $user = $activityItem->getUser();
            $name = $user->getName();
            $avatar = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/avatar/' . $user->getAvatar();
            $data = $activityItem->getData();
            $created = $activityItem->getCreatedAt();

            $activityArray[$groupNumber]['type'] = $type;
            $activityArray[$groupNumber]['created'] = $created;

            if ($type == ActivityType::Like) {
                if ($groupNumber != $previousGroupNumber) {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Comment) {
                $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                $activityArray[$groupNumber]['parent']['data'] = $data;
            } else if ($type == ActivityType::Joined) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;#
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Added) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            }

            $previousGroupNumber = $groupNumber;
        }

        return $activityArray;
    }

First we refactor $data.

 $data = $activityItem->getData();

$data is an array stored in the database like this:

Joined Data:

a:2:{s:6:"userId";s:1:"1";s:8:"username";s:8:"mbwagner";}

Watchlisted Data

a:5:{s:13:"documentaryId";i:728;s:16:"documentaryTitle";s:24:"The U.S. vs. John Lennon";s:18:"documentaryExcerpt";s:160:"In retrospect, it seems absurd that the United States government felt so threatened by the presence of John Lennon that they tried to have him deported. But tha";s:20:"documentaryThumbnail";s:24:"cover/usvsjohnlennon.jpg";s:15:"documentarySlug";s:22:"the-u-s-vs-john-lennon";}

The problem with this way is the data is hardcoded into the database, what happens if the data changes? So I decide to populate data on the fly.

Replace:

 $data = $activityItem->getData();

with:

$dataStrategyContext = new DataStrategyContext(
                $type,
                $this->request,
                $this->documentaryService,
                $this->commentService);
$data = $dataStrategyContext->createData($activityItem);

Here we use the Strategy Pattern to populate data.

class DataStrategyContext
{
    /**
     * @var StrategyInterface|null
     */
    private $strategy = null;

    public function __construct(
        string $type,
        Request $request,
        DocumentaryService $documentaryService,
        CommentService $commentService)
    {
        switch ($type) {
            case ActivityType::WATCHLIST:
                $this->strategy = new StrategyWatchlist(
                    $request,
                    $documentaryService
                );
            break;
            case ActivityType::COMMENT:
                $this->strategy = new StrategyComment(
                    $request,
                    $commentService
                );
            break;
            case ActivityType::JOINED:
                $this->strategy = new StrategyJoined();
            break;
            case ActivityType::ADDED:
                $this->strategy = new StrategyAdded(
                    $documentaryService
                );
            break;
        }
    }

    /**
     * @param Activity $activityEntity
     * @return mixed
     */
    public function createData(Activity $activityEntity)
    {
        return $this->strategy->createData($activityEntity);
    }
}

StrategyWatchlist would look like the following:

class StrategyWatchlist implements StrategyInterface
{
    /**
     * @var Request
     */
    private $request;

    /**
     * @var DocumentaryService
     */
    private $documentaryService;

    /**
     * @param Request $request
     * @param DocumentaryService $documentaryService
     */
    public function __construct(
        Request $request,
        DocumentaryService $documentaryService)
    {
        $this->request = $request;
        $this->documentaryService = $documentaryService;
    }

    /**
     * @param Activity $activityEntity
     * @return Data
     */
    public function createData(Activity $activityEntity)
    {
        $documentaryId = $activityEntity->getObjectId();
        $documentary = $this->documentaryService->getDocumentaryById($documentaryId);

        $watchlistData = new WatchlistData();
        $watchlistData->setDocumentaryId($documentary->getId());
        $watchlistData->setDocumentaryTitle($documentary->getTitle());
        $watchlistData->setDocumentarySlug($documentary->getSlug());
        $watchlistData->setDocumentarySummary($documentary->getSummary());
        $poster = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $documentary->getPoster();
        $watchlistData->setDocumentaryPoster($poster);

        return $watchlistData;
    }
}

The next thing is to refactor parent and children from this:

if ($type == ActivityType::Like) {
                if ($groupNumber != $previousGroupNumber) {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $data['documentaryThumbnail'] = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/posters/' . $data['documentaryThumbnail'];
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Comment) {
                $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                $activityArray[$groupNumber]['parent']['data'] = $data;
            } else if ($type == ActivityType::Joined) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;#
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            } else if ($type == ActivityType::Added) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityArray[$groupNumber]['parent']['data'] = $data;
                    $activityArray[$groupNumber]['parent']['user']['name'] = $name;
                    $activityArray[$groupNumber]['parent']['user']['avatar'] = $avatar;
                } else {
                    $child['data'] = $data;
                    $child['user']['name'] = $name;
                    $child['user']['avatar'] = $avatar;
                    $activityArray[$groupNumber]['child'][] = $child;
                }
            }

            $previousGroupNumber = $groupNumber;
        }

After refactoring and creating ActivityParent and ActivityChild it should look like this:

if ($type == ActivityType::Like) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);

		$activityArray[$groupNumber]['parent'] = $activityParent->toArray();
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
} else if ($type == ActivityType::Comment) {
	$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
} else if ($type == ActivityType::Joined) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
} else if ($type == ActivityType::Added) {
	if ($groupNumber != $previousGroupNumber) {
		$activityParent = new ActivityParent();
		$activityParent->setName($name);
		$activityParent->setUsername($username);
		$activityParent->setAvatar($avatar);
		$activityParent->setData($data);
	} else {
		$activityChild = new ActivityChild();
		$activityChild->setData($data);
		$activityChild->setUsername($username);
		$activityChild->setName($name);
		$activityChild->setAvatar($avatar);

		$activityArray[$groupNumber]['child'][] = $activityChild->toArray();
	}
}

$previousGroupNumber = $groupNumber;
}

Notice there are duplicate times when we create ActivityParent and ActivityChild. Only certain types have children:

class ActivityType
{
    const LIKE = "like";
    const COMMENT = "comment";
    const FOLLOW = "follow";
    const JOINED = "joined";
    const ADDED = "added";

    /**
     * @return array
     */
    public static function getAllTypes()
    {
        return [
            self::LIKE,
            self::ADDED,
            self::JOINED,
            self::COMMENT,
            self::FOLLOW
        ];
    }

    /**
     * @return array
     */
    public static function getTypesWithChildren()
    {
        return [
            self::LIKE,
            self::JOINED,
            self::ADDED
        ];
    }

    /**
     * @param string $type
     * @return bool
     */
    public static function hasChildren(string $type)
    {
        return in_array($type, self::getTypesWithChildren());
    }
}

Then we update the convertActivityToArray() function to this:


            $hasChildren = ActivityType::hasChildren($type);
            if ($hasChildren) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityParent = new ActivityParent();
                    $activityParent->setName($name);
                    $activityParent->setUsername($username);
                    $activityParent->setAvatar($avatar);
                    $activityParent->setData($data);

                    $activityArray[$groupNumber]['parent'] = $activityParent->toArray();

                } else {
                    $activityChild = new ActivityChild();
                    $activityChild->setData($data);
                    $activityChild->setUsername($username);
                    $activityChild->setName($name);
                    $activityChild->setAvatar($avatar);

                    $activityArray[$groupNumber]['child'][] = $activityChild->toArray();
                }
            } else {
                $activityParent = new ActivityParent();
                $activityParent->setName($name);
                $activityParent->setUsername($username);
                $activityParent->setAvatar($avatar);
                $activityParent->setData($data);

                $activityArray[$groupNumber]['parent'] = $activityParent->toArray();
            }

Notice ActivityParent and ActivityChild are doing the same thing so I refactored it to just ActivityObject. Plus I moved it above the IF statements.

$activityObject = new ActivityObject();
$activityObject->setName($name);
$activityObject->setUsername($username);
$activityObject->setAvatar($avatar);
$activityObject->setData($data);

$hasChildren = ActivityType::hasChildren($type);
if ($hasChildren) {
	if ($groupNumber != $previousGroupNumber) {
		$activityArray[$groupNumber]['parent'] = $activityObject->toArray();
	} else {
		$activityArray[$groupNumber]['child'][] = $activityObject->toArray();
	}
} else {
	$activityArray[$groupNumber]['parent'] = $activityObject->toArray();
}

Next we add ActivityItemOject to hold the array keys and values.

class ActivityItemObject
{
    /**
     * @var string
     */
    private $type;

    /**
     * @var \DateTime
     */
    private $created;

    /**
     * @var ActivityObject
     */
    private $parent;

    /**
     * @var ActivityObject[]
     */
    private $children;

    public function __construct()
    {
        $this->children = [];
    }
.....

    /**
     * @return array
     */
    public function toArray()
    {
        $children = [];
        foreach ($this->children as $child) {
            $children[] = $child->toArray();
        }

        $array = [
            'type' => $this->type,
            'created' => $this->created,
            'parent' => $this->parent->toArray(),
            'child' => $children
        ];

        return $array;
    }

And finally the completed code:


    /**
     * @param ArrayCollection|Activity[] $activity
     * @return array
     */
    private function convertActivityToArray(array $activity)
    {
        $activityMap = [];

        $previousGroupNumber = 0;
        /** @var Activity $activityEntity */
        foreach ($activity as $activityItem) {
            $groupNumber = $activityItem->getGroupNumber();

            if (array_key_exists($groupNumber, $activityMap) != null) {
                $activityItemObject = $activityMap[$groupNumber];
            } else {
                $activityItemObject = new ActivityItemObject();
            }

            $type = $activityItem->getType();
            $created = $activityItem->getCreatedAt();

            $activityItemObject->setType($type);
            $activityItemObject->setCreated($created);

            $dataStrategyContext = new DataStrategyContext(
                $type,
                $this->request,
                $this->documentaryService,
                $this->commentService);
            $data = $dataStrategyContext->createData($activityItem);

            $user = $activityItem->getUser();
            $name = $user->getName();
            $avatar = $this->request->getScheme() .'://' . $this->request->getHttpHost() . $this->request->getBasePath() . '/uploads/avatar/' . $user->getAvatar();
            $username = $user->getUsername();

            $activityObject = new ActivityObject();
            $activityObject->setName($name);
            $activityObject->setUsername($username);
            $activityObject->setAvatar($avatar);
            $activityObject->setData($data);

            $hasChildren = ActivityType::hasChildren($type);
            if ($hasChildren) {
                if ($groupNumber != $previousGroupNumber) {
                    $activityItemObject->setParent($activityObject);
                } else {
                    $activityItemObject->addChild($activityObject);
                }
            } else {
                $activityItemObject->setParent($activityObject);
            }

            $activityMap[$groupNumber] = $activityItemObject;

            $previousGroupNumber = $groupNumber;
        }

        $display = [];
        /** @var ActivityItemObject $value */
        foreach ($activityMap as $key => $value) {
            $display[$key] = $value->toArray();
        }

        return $display;
    }
 

Add New Documentary Series

Here’s the code for creating new documentary series.

import { YoutubeService } from './../../../../services/youtube.service';
import { OMDBService } from './../../../../services/omdb.service';
import { AngularEditorConfig } from '@kolkov/angular-editor';
import { FormBuilder, FormControl, Validators, FormArray, FormGroup } from '@angular/forms';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { CategoryService } from './../../../../services/category.service';
import { VideoSourceService } from './../../../../services/video-source.service';
import { HttpParams } from '@angular/common/http';
import { YearService } from './../../../../services/year.service';
import { UserService } from './../../../../services/user.service';
import { DocumentaryService } from './../../../../services/documentary.service';
import { Router, ActivatedRoute } from '@angular/router';
import { Episodic } from './../../../../models/episodic.model';
import { Documentary } from 'src/app/models/documentary.model';
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { Location } from "@angular/common";

@Component({
  selector: 'app-documentary-add-episodic',
  templateUrl: './documentary-add-episodic.component.html',
  styleUrls: ['./documentary-add-episodic.component.css']
})
export class DocumentaryAddEpisodicComponent implements OnInit {

  private documentary: Documentary;

  private form: FormGroup;
  private imdbForm: FormGroup;
  private youtubeForm: FormGroup;

  private submitted = false;
  private errors = null;

  private posterImgURL;
  private wideImgURL;
  private thumbnailImgURLDict = {};

  private page;
  private slug;
  private editMode = false;

  private me;

  private config: any;

  private seasonIndex;
  private episodeIndex;

  editorConfig: AngularEditorConfig = {
    editable: true,
    spellcheck: true,
    height: '25rem',
    minHeight: '5rem',
    placeholder: 'Enter text here...',
    translate: 'no',
    uploadUrl: 'v1/images', // if needed
  };

  private showPage = false;
  private hasToggledForm = false;
  private showForm = false;
  private showAddTitleButton = true;
  private showDocumentaries = true;

  private isFetchingYears = false;
  private isFetchingVideoSources = false;
  private isFetchingCategories = false;
  private isFetchingDocumentaries = false;

  private isFetchingDocumentariesFromIMDB = false;
  private isFetchingDocumentaryFromIMDB = false;
  private showSearchedDocumentariesFromIMDB = false;
  private showSearchedDocumentaryFromIMDB = false;

  private searchedDocumentariesFromIMDB;
  private searchedDocumentaryFromIMDB;

  private isFetchingVideosFromYoutube = false;
  private showSearchedVideosFromYoutube = false;
  private searchedVideosFromYoutube;

  private years;
  private videoSources;
  private categories;

  private myDocumentaries;

  private queryParamsSubscription;
  private routeParamsSubscription;
  private documentaryBySlugSubscription;
  private meSubscription;
  private videoSourcesSubscription;
  private categoriesSubscription;
  private getByImdbIdSubscription;
  private ombdSearchSubscription;
  private youtubeByIdSubscription;
  private youtubeSearchSubscription;

  private closeResult: string;

  private seasonNumber = 1;
  private episodeNumber = 1;

  private imdbType;

  constructor(
    private documentaryService: DocumentaryService,
    private userService: UserService,
    private yearService: YearService,
    private videoSourceService: VideoSourceService,
    private categoryService: CategoryService,
    private omdbService: OMDBService,
    private youtubeService: YoutubeService,
    private route: ActivatedRoute,
    private cd: ChangeDetectorRef,
    private router: Router,
    private location: Location,
    private modalService: NgbModal,
    private fb: FormBuilder
  ) { }

  ngOnInit() {
    this.initModel();

    this.queryParamsSubscription = this.route
      .queryParams
      .subscribe(params => {
        this.page = +params['page'] || 1;

        this.routeParamsSubscription = this.route
          .paramMap
          .subscribe(params => {
            this.slug = params['params']['slug'];
            this.editMode = this.slug != null;

            if (this.editMode) {
              this.documentaryBySlugSubscription = this.documentaryService
                .getDocumentaryBySlug(this.slug)
                .subscribe((result: any) => {
                  this.documentary = result;
                  this.toggleForm();
                  this.showPage = true;
                });
            } else {
              this.meSubscription = this.userService
                .getMe()
                .subscribe(me => {
                  this.me = me;

                  if (!this.hasToggledForm) {
                    this.fetchDocumentaries();
                    this.showPage = true;
                  }
                })
            }
          })
      })
  }

  initModel() {
    this.documentary = new Documentary();
    let episodic = new Episodic();
    this.documentary.episodic = episodic;
  }

  toggleForm() {
    this.showAddTitleButton = false;
    this.showDocumentaries = false;

    this.showForm = !this.showForm;

    this.initYears();
    this.initVideoSources();
    this.initCategories();
    this.initForm();
  }

  initForm(seasons = null) {
    let title = this.documentary.title;
    let category = this.documentary.category;
    let storyline = this.documentary.storyline;
    let summary = this.documentary.summary;
    // let videoSource = null;
    //if (this.documentary.standalone.videoSource) {
    //  videoSource = this.documentary.standalone.videoSource.id
    // }
    //let videoId = this.documentary.standalone.videoId;
    let year = this.documentary.year;
    let yearFrom = this.documentary.yearFrom;
    let yearTo = this.documentary.yearTo;
    //let length = this.documentary.length;
    let poster = this.documentary.poster;
    this.posterImgURL = this.documentary.poster;
    let wideImage = this.documentary.wideImage;
    this.wideImgURL = this.documentary.wideImage;
    let imdbId = this.documentary.imdbId;

    this.form = this.fb.group({
      'title': new FormControl(title, [Validators.required]),
      'category': new FormControl(category, [Validators.required]),
      'storyline': new FormControl(storyline, [Validators.required]),
      'summary': new FormControl(summary, [Validators.required]),
      'yearFrom': new FormControl(yearFrom, [Validators.required]),
      'yearTo': new FormControl(yearTo, [Validators.required]),
      'poster': new FormControl(poster, [Validators.required]),
      'wideImage': new FormControl(wideImage, [Validators.required]),
      'imdbId': new FormControl(imdbId),
      'seasons': this.fb.array([], Validators.required)
    });

    if (seasons != null) {
      seasons.forEach(season => {
        this.addSeason(season);
      });
    }

    console.log("this.thumbnailImgURLDict");
    console.log(this.thumbnailImgURLDict);
  }

  getEpisodeNumber(episode) {
    return episode.value.number;
  }

  getSeasonNumber(season) {
    return season.value.number;
  }

  addSeason(season = null) {
    if (season != null) {
      this.seasonNumber = season.number;
    }

    let control = <FormArray>this.form.controls.seasons;
    control.push(
      this.fb.group({
        'number': new FormControl(this.seasonNumber, [Validators.required]),
        'episodes': this.fb.array([], Validators.required)
      })
    );

    if (season != null) {
      let episodes = season.episodes;
      if (season != null && episodes != null) {
        episodes.forEach(episode => {
          let episodesControl = control.at(this.seasonNumber - 1).get('episodes');
          this.addEpisode(episodesControl, season, episode);
        })
      }
    }

    if (season == null) {
      this.seasonNumber++;
    }
  }

  deleteSeason(number) {
    var seasonsFormArray = this.form.get("seasons") as FormArray;

    let index = 0;
    seasonsFormArray.value.forEach(seasonArray => {
      if (number == seasonArray.number) {
        seasonsFormArray.removeAt(index);
        return;
      }
      index++;
    });
  }

  deleteEpisode(control, index) {
    control.removeAt(index);
  }

  insertEpisode(control, index) {
    let title;
    let storyline;
    let summary;
    let year;
    let length;
    let imdbId;
    let videoId;
    let videoSource = 2;
    let thumbnail;
    let episodeNumber;

    control.insert(index,
      this.fb.group({
        'number': new FormControl(episodeNumber, [Validators.required]),
        'title': new FormControl(title, [Validators.required]),
        'imdbId': new FormControl(imdbId),
        'storyline': new FormControl(storyline, [Validators.required]),
        'summary': new FormControl(summary, [Validators.required]),
        'length': new FormControl(length, [Validators.required]),
        'year': new FormControl(year, [Validators.required]),
        'videoSource': new FormControl(videoSource, [Validators.required]),
        'videoId': new FormControl(videoId, [Validators.required]),
        'thumbnail': new FormControl(thumbnail, [Validators.required]),
      }));
  }

  addEpisode(control, season, episode = null) {
    let title;
    let storyline;
    let summary;
    let year;
    let length;
    let imdbId;
    let videoId;
    let videoSource = 2;
    let thumbnail;
    let episodeNumber;

    if (episode != null) {
      episodeNumber = episode.number;
      title = episode.title;
      imdbId = episode.imdbId;
      thumbnail = episode.thumbnail;
      summary = episode.summary;
      storyline = episode.storyline;
      year = episode.year;
      videoId = episode.videoId;
      videoSource = 2;
      length = episode.length;

      if (this.thumbnailImgURLDict[season.number] == undefined) {
        this.thumbnailImgURLDict[season.number] = {};
      }
      if (this.thumbnailImgURLDict[season.number][episode.number] == undefined) {
        this.thumbnailImgURLDict[season.number][episode.number] = {};
      }
      this.thumbnailImgURLDict[season.number][episode.number] = thumbnail;
    }

    control.push(
      this.fb.group({
        'number': new FormControl(episodeNumber, [Validators.required]),
        'title': new FormControl(title, [Validators.required]),
        'imdbId': new FormControl(imdbId),
        'storyline': new FormControl(storyline, [Validators.required]),
        'summary': new FormControl(summary, [Validators.required]),
        'length': new FormControl(length, [Validators.required]),
        'year': new FormControl(year, [Validators.required]),
        'videoSource': new FormControl(videoSource, [Validators.required]),
        'videoId': new FormControl(videoId, [Validators.required]),
        'thumbnail': new FormControl(thumbnail, [Validators.required]),
      }));

  }

  get f() { return this.form.controls; }

  getThumbnailForSeasonAndEpsiode(seasonNumber: number, episodeNumber: number) {
    if (this.thumbnailImgURLDict[seasonNumber] == undefined) {
      this.thumbnailImgURLDict[seasonNumber] = {};
    }
    return this.thumbnailImgURLDict[seasonNumber][episodeNumber];
  }

  onThumbnailChange(event, seasonIndex, episodeIndex) {
    console.log(event);
    let reader = new FileReader();

    if (event.target.files && event.target.files.length) {
      const [file] = event.target.files;
      reader.readAsDataURL(file);

      reader.onload = () => {
        var seasonsFormArray = this.form.get("seasons") as FormArray;
        var episodesFormArray = seasonsFormArray.at(seasonIndex).get("episodes") as FormArray;
        episodesFormArray.at(episodeIndex)['controls']['thumbnail'].patchValue(reader.result);

        let seasonNumber = seasonsFormArray.at(seasonIndex).value.number;
        let episodeNumber = episodesFormArray.at(episodeIndex).value.number;

        if (this.thumbnailImgURLDict[seasonNumber] == undefined) {
          this.thumbnailImgURLDict[seasonNumber] = {};
        }
        this.thumbnailImgURLDict[seasonNumber][episodeNumber] = reader.result;
      }

      this.cd.markForCheck();

    };
  }

  openIMDBModal(content, imdbType) {
    console.log(imdbType);
    this.imdbType = imdbType;
    this.initIMDBFrom();
    console.log(content);
    this.modalService.open(content, { ariaLabelledBy: 'modal-omdb' }).result.then((result) => {
      this.closeResult = `Closed with: ${result}`;
    }, (reason) => {
      this.closeResult = `Dismissed ${reason}`;
    });
  }


  initIMDBFrom() {
    let title = this.form.value.title;

    this.imdbForm = new FormGroup({
      'title': new FormControl(title, [Validators.required])
    });
  }

  imdbView(imdbId) {
    console.log("imdbId");
    console.log(imdbId);
    this.isFetchingDocumentaryFromIMDB = true;
    this.showSearchedDocumentariesFromIMDB = false;
    this.showSearchedDocumentaryFromIMDB = true;

    let imdbType = this.imdbType;
    this.omdbService.getByImdbId(imdbId, imdbType)
      .subscribe((result: any) => {
        this.searchedDocumentaryFromIMDB = result;
        this.isFetchingDocumentaryFromIMDB = false;
        console.log("searchedDocumentaryFromIMDB");
        console.log(this.searchedDocumentaryFromIMDB);
      })
  }

  imdbSelect() {
    let selectedDocumentary = this.searchedDocumentaryFromIMDB;
    console.log("selectedDocumentary");
    console.log(selectedDocumentary);
    console.log("this.form");
    console.log(this.form);
    if (this.form.value.title == null) {
      this.documentary.title = selectedDocumentary.title;
    } else {
      this.documentary.title = this.form.value.title;
    }

    if (this.form.value.imdbId != selectedDocumentary.imdbId) {
      this.documentary.imdbId = selectedDocumentary.imdbId;
    }

    if (this.form.value.storyline == null) {
      this.documentary.storyline = selectedDocumentary.storyline;
    } else {
      this.documentary.storyline = this.form.value.storyline;
    }

    if (this.form.value.yearFrom == null) {
      this.documentary.yearFrom = selectedDocumentary.yearFrom;
    } else {
      this.documentary.yearFrom = this.form.value.yearFrom;
    }

    if (this.form.value.yearTo == null) {
      this.documentary.yearTo = selectedDocumentary.yearTo;
    } else {
      this.documentary.yearTo = this.form.value.yearTo;
    }

    if (this.form.value.poster == null) {
      this.documentary.poster = selectedDocumentary.poster;
      this.posterImgURL = selectedDocumentary.poster;
    } else {
      this.documentary.poster = this.form.value.poster;
      this.posterImgURL = this.form.value.poster;
    }

    let seasons = selectedDocumentary.seasons;
    this.initForm(seasons);

    this.modalService.dismissAll();
  }

  searchOMDB() {
    this.isFetchingDocumentariesFromIMDB = true;
    this.showSearchedDocumentaryFromIMDB = false;
    this.showSearchedDocumentariesFromIMDB = true;

    let titleOrId = this.imdbForm.value.title;
    let imdbType = this.imdbType;

    this.getByImdbIdSubscription = this.omdbService.getByImdbId(titleOrId, imdbType)
      .subscribe((result: any) => {
        result = [result];
        this.searchedDocumentariesFromIMDB = result;
        this.isFetchingDocumentariesFromIMDB = false;
      },
        (error) => {
          this.ombdSearchSubscription = this.omdbService.getSearchedDocumentaries(titleOrId, imdbType)
            .subscribe((result: any) => {
              this.searchedDocumentariesFromIMDB = result;
              this.isFetchingDocumentariesFromIMDB = false;
            });
        });
  }


  openYoutubeModal(content, seasonIndex: number, episodeIndex: number) {
    this.seasonIndex = seasonIndex;
    this.episodeIndex = episodeIndex;

    this.initYoutubeForm();

    this.modalService.open(content, { ariaLabelledBy: 'modal-youtube' }).result.then((result) => {
      this.closeResult = `Closed with: ${result}`;
    }, (reason) => {
      this.closeResult = `Dismissed ${reason}`;
    });
  }

  initYoutubeForm() {
    let seasonIndex = this.seasonIndex;
    let episodeIndex = this.episodeIndex;

    let seasonsFormArray = this.form.get("seasons") as FormArray;
    let episodesFormArray = seasonsFormArray.at(seasonIndex).get("episodes") as FormArray;
    let titleOrId = episodesFormArray.at(episodeIndex)['controls']['title'].value;

    this.youtubeForm = new FormGroup({
      'title': new FormControl(titleOrId, [Validators.required])
    });
  }

  searchYoutube() {
    this.isFetchingVideosFromYoutube = true;
    this.showSearchedVideosFromYoutube = true;

    let titleOrId = this.youtubeForm.value.title;

    this.youtubeByIdSubscription = this.youtubeService.getById(titleOrId)
      .subscribe((result: any) => {
        this.searchedVideosFromYoutube = result['items'];
        this.isFetchingVideosFromYoutube = false;
      }, (error) => {
        this.youtubeSearchSubscription = this.youtubeService.getSearchedDocumentaries(titleOrId)
          .subscribe((result: any) => {
            this.searchedVideosFromYoutube = result['items'];
            this.isFetchingVideosFromYoutube = false;
          });
      });
  }

  youtubeSelect(selectedVideo) {
    let seasonIndex = this.seasonIndex;
    let episodeIndex = this.episodeIndex;

    let seasonsFormArray = this.form.get("seasons") as FormArray;
    let episodesFormArray = seasonsFormArray.at(seasonIndex).get("episodes") as FormArray;

    let title = episodesFormArray.at(episodeIndex)['controls']['title'].value;
    if (title == null || !title.trim()) {
      let selectedTitle = selectedVideo.snippet.title;
      episodesFormArray.at(episodeIndex)['controls']['title'].patchValue(selectedTitle);
    }

    let videoId = episodesFormArray.at(episodeIndex)['controls']['videoId'].value;
    if (videoId == null || !videoId.trim()) {
      let selectedVideoId = selectedVideo.id.videoId;
      episodesFormArray.at(episodeIndex)['controls']['videoId'].patchValue(selectedVideoId);
    }

    let thumbnail = episodesFormArray.at(episodeIndex)['controls']['thumbnail'].value;
    if (thumbnail == null || !thumbnail.trim()) {
      let selectedThumbnail = selectedVideo.snippet.thumbnails.default.url;
      episodesFormArray.at(episodeIndex)['controls']['thumbnail'].patchValue(selectedThumbnail);

      let seasonNumber = seasonsFormArray.at(seasonIndex).value.number;
      let episodeNumber = episodesFormArray.at(episodeIndex).value.number;

      if (this.thumbnailImgURLDict[seasonNumber] == undefined) {
        this.thumbnailImgURLDict[seasonNumber] = {};
      }
      this.thumbnailImgURLDict[seasonNumber][episodeNumber] = selectedThumbnail;
    }

    this.episodeIndex = null;
    this.seasonIndex = null;
    this.modalService.dismissAll();
  }

  fetchDocumentaries() {
    if (this.editMode) {
      this.showDocumentaries = false;
      return;
    }

    this.isFetchingDocumentaries = true;

    let params = new HttpParams();

    params = params.append('page', this.page.toString());

    this.location.go(this.router.url.split("?")[0], params.toString());

    this.documentaryService.getMyEpisodicDocumentaries(params, this.me.username)
      .subscribe(result => {
        this.config = {
          itemsPerPage: 5,
          currentPage: this.page,
          totalItems: result['count_results']
        };
        this.myDocumentaries = result['items'];

        this.isFetchingDocumentaries = false;
        this.showDocumentaries = true;
        this.showAddTitleButton = true;
      })
  }

  pageChanged(event) {
    this.config.currentPage = event;
    this.page = event;
    this.fetchDocumentaries();
  }

  initYears() {
    this.isFetchingYears = true;

    this.years = this.yearService.getAllYearsForForm();

    this.isFetchingYears = false;
  }

  initVideoSources() {
    this.isFetchingVideoSources = true;

    let params: HttpParams;
    this.videoSourcesSubscription = this.videoSourceService.getAll(params)
      .subscribe(result => {
        this.videoSources = result;
        console.log("this.videoSources");
        console.log(this.videoSources);

        this.isFetchingVideoSources = false;
      });
  }

  initCategories() {
    this.isFetchingCategories = true;

    let params: HttpParams;
    this.categoriesSubscription = this.categoryService.getAll(params)
      .subscribe(result => {
        this.categories = result;

        this.isFetchingCategories = false;
      });
  }

  onSubmit() {
    this.errors = null;

    console.log(this.f);
    console.log(this.form.value);

    this.submitted = true;

    let formValue = this.form.value;

    if (formValue.seasons.length === 0) {
      this.errors = "You must add a season";
    }

    if (formValue.seasons[0].episodes.length === 0) {
      this.errors = "You must add an episode";
    }

    if (!this.form.valid) {
      return;
    }

    this.errors = null;

    if (this.editMode) {
      this.documentaryService.editEpisodicDocumentary(this.documentary.id, formValue)
        .subscribe((result: any) => {
          //this.reset();
          this.router.navigate(["/add/episodic"]);
        },
          (error) => {
            console.log(error);
            this.errors = error.error;
          });
    } else {
      this.documentaryService.createEpisodicDocumentary(formValue)
        .subscribe((result: any) => {
          //this.reset();
          this.router.navigate(["/add/episodic"]);
        },
          (error) => {
            console.log(error);
            this.errors = error.error;
          });
    }
  }

  ngOnDestroy() {
    this.queryParamsSubscription.unsubscribe();
    this.routeParamsSubscription.unsubscribe();
    if (this.documentaryBySlugSubscription != null) {
      this.documentaryBySlugSubscription.unsubscribe();
    }
    if (this.meSubscription != null) {
      this.meSubscription.unsubscribe();
    }
    if (this.getByImdbIdSubscription != null) {
      this.getByImdbIdSubscription.unsubscribe();
    }
    if (this.ombdSearchSubscription != null) {
      this.ombdSearchSubscription.unsubscribe();
    }
    this.meSubscription.unsubscribe();
  }
}
 

Admin Documentaries with Query Params

See also: The Criteria Pattern

import { Status } from './../../models/status.model';
import { CategoryService } from './../../services/category.service';
import { VideoSourceService } from './../../services/video-source.service';
import { VideoSource } from './../../models/video-source.model';
import { ActivatedRoute, Router } from '@angular/router';
import { DocumentaryService } from './../../services/documentary.service';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { HttpParams } from '@angular/common/http';
import { Documentary } from './../../models/documentary.model';
import { Location } from "@angular/common";
import { Category } from 'src/app/models/category.model';

@Component({
  selector: 'app-admin-documentaries',
  templateUrl: './admin-documentaries.component.html',
  styleUrls: ['./admin-documentaries.component.css']
})
export class AdminDocumentariesComponent implements OnInit, OnDestroy {
  
  private documentariesSubscription;
  private queryParamsSubscription;
  private videoSourcesSubscription;
  private categoriesSubscription;
  public documentaries: Array<Documentary>;
  public videoSources: Array<VideoSource>;
  public categories: Array<Category>;
  public statuses: Array<Status> = [
    { id: 'pending', name: 'Pending' },
    { id: 'publish', name: 'Published' }
  ];
  public featuredOptions = [
    { id: true },
    { id: false }
  ];
  config: any;
  private page;
  private videoSource;
  private previousVideoSource;
  private previousCategory;
  private category;
  private status;
  private previousStatus;
  private featured;
  private previousFeatured;

  constructor(
    private service: DocumentaryService,
    private videoSourceService: VideoSourceService,
    private categoryService: CategoryService,
    private route: ActivatedRoute,
    private location: Location,
    private router: Router) { }

  ngOnInit() {
    this.queryParamsSubscription = this.route
      .queryParams
      .subscribe(params => {
        this.page = +params['page'] || 1;
        this.videoSource = +params['videoSource'] || 'all';
        this.category = +params['category'] || 'all';
        this.status = params['status'] || 'all';
        this.featured = params['featured'] || 'all';
        this.fetchVideoSources();
        this.fetchCategories();
        this.fetchDocumentaries();
      })
  }

  fetchDocumentaries() {
    let params = new HttpParams();
    if (this.videoSource) {
      if (this.videoSource != 'all') {
        params = params.append('videoSource', this.videoSource.toString());
        if (this.videoSource != this.previousVideoSource) {
          this.page = 1;
        }
     }
      this.previousVideoSource = this.videoSource;
    }
    if (this.category) {
      if (this.category != 'all') {
        params = params.append('category', this.category.toString());
        if (this.category != this.previousCategory) {
          this.page = 1;
        }
      }
      this.previousCategory = this.category;
    }
    if (this.status) {
      if (this.status != 'all') {
        params = params.append('status', this.status.toString());
        if (this.status != this.previousStatus) {
          this.page = 1;
        }
      }
      this.previousStatus = this.status;
    }
    if (this.featured) {
      if (this.featured != 'all') {
        params = params.append('featured', this.featured.toString());
        if (this.featured != this.previousFeatured) {
          this.page = 1;
        }
      }
      this.previousFeatured = this.featured;
    }
    
    params = params.append('page', this.page.toString());
    
    this.location.go(this.router.url.split("?")[0], params.toString());

    this.documentariesSubscription = this.service.getAllDocumentaries(params)
      .subscribe(
          result => {
            this.config = {
              itemsPerPage: 12,
              currentPage: this.page,
              totalItems: result['count_results']
            };
            this.documentaries = result['items'];
            console.log(result);
          }
      );
  }

  fetchVideoSources() {
    this.videoSourcesSubscription = this.videoSourceService.getAllVideoSources()
      .subscribe(result => {
        this.videoSources = <any> result;
      });
  }

  fetchCategories() {
    this.categoriesSubscription = this.categoryService.getAllCategories()
      .subscribe(result => { 
        this.categories = <any> result;
      });
  }

  pageChanged(event) {
    console.log(event);
    this.config.currentPage = event;
    this.page = event;
    this.fetchDocumentaries();
  }

  onVideoSourceSelected(value: string) {
    this.videoSource = value;
    this.fetchDocumentaries();
  }

  onCategoriesSelected(value: string) {
    this.category = value;
    this.fetchDocumentaries();
  }

  onStatusSelected(value: string) {
    this.status = value;
    this.fetchDocumentaries();
  }

  onFeaturedSelected(value: string) {
    this.featured = value;
    this.fetchDocumentaries();
  }

  ngOnDestroy() {
    this.documentariesSubscription.unsubscribe();
    this.queryParamsSubscription.unsubscribe();
    this.videoSourcesSubscription.unsubscribe();
    this.categoriesSubscription.unsubscribe();
  }
}
 

Technical Test – Javascript (Frontend) & PHP (API Backend)

Please complete the following technical task. I will be reviewing it to asses the following:

* Your ability to follow vague instructions and use your own initiative.
* Your ability to write documented PHP code.
* Your ability to design and abstract database tables.
* Your ability to ability to write a simple RESTful API.
* Your ability to use jQuery and ajax to interact with your API.
* Your ability to write integration documentation for your code.
* The overall robustness of your code (error handling, validation and security especially).
Here is the task:

* Create a simple RESTful API and PHP 7 application which serves to create, retrieve, update and delete employees to two fictional companies "Company A" and "Company B".

* A database with company and employee tables have been provided for you. Use these tables, but feel free to add any other tables or columns you need.

* The RESTful API must be the backend of your PHP app.

* Ensure that your API returns the proper HTTP status where required: 200/400/403/404/405/406/500 - this will be tested.

* In the frontend of your PHP app, create a simple list view to show all employees from all companies, but provide an option to filter these by company and sort them by first name or last name. Also include action buttons to add a new employee and update/delete existing employees.

* Use bootstrap components for style and responsive layout.

* Use a modal dialog to display the form which creates or updates a user.

* When deleting a user, display a message to ask the user if they are sure they want to proceed.

* When creating, updating or deleting a user, use jQuery ajax to call your API which in turn updates the database.

* When complete, put the project in a .git repository and add a readme.md to explain how to install and run the project on a vanilla linux box.

Source code: https://github.com/JonnyD/Technical-Test-Javascript-and-PHP

Employees:

You can filter by Company:

and sort by first name or last name:

You can read each employee:

Edit:

And Delete

Create employee:

Here is the MySQL to create and populate employees and companies:

CREATE DATABASE  IF NOT EXISTS `test` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `test`;

--
-- Table structure for table `company`
--

DROP TABLE IF EXISTS `company`;
/*!40101 SET @saved_cs_client     = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `company` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

INSERT INTO `company` (`id`, `name`) VALUES (1, 'Company A');
INSERT INTO `company` (`id`, `name`) VALUES (2, 'Company B');

--
-- Table structure for table `employee`
--

DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(45) DEFAULT NULL,
  `last_name` varchar(45) DEFAULT NULL,
  `job_title` varchar(45) DEFAULT NULL,
  `email` varchar(45) DEFAULT NULL,
  `phone` varchar(45) DEFAULT NULL,
  `address_line_1` varchar(45) DEFAULT NULL,
  `address_line_2` varchar(45) DEFAULT NULL,
  `town_city` varchar(45) DEFAULT NULL,
  `county_region` varchar(45) DEFAULT NULL,
  `country` varchar(45) DEFAULT NULL,
  `postcode` varchar(45) DEFAULT NULL,
  `company_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
/*!40101 SET character_set_client = @saved_cs_client */;

ALTER TABLE employee ADD FOREIGN KEY (company_id) REFERENCES company(id);

INSERT INTO employee (id, first_name, last_name, job_title, email, phone, address_line_1, address_line_2, town_city, county_region, country, postcode, company_id)
VALUES (1, 'Jonathan', 'Devine', 'Software Developer', 'contact@jonnydevine.com', '0862041801', '23 Windmill Road', NULL, 'Drogheda', 'Louth', 'Ireland', 'ABC123', 1);
INSERT INTO employee (id, first_name, last_name, job_title, email, phone, address_line_1, address_line_2, town_city, county_region, country, postcode, company_id)
VALUES (2, 'Steven', 'Andrews', 'Front End Developer', 'contact@steven.com', '0862454944', '2 Fake Road', NULL, 'Dublin', 'Dublin', 'Ireland', 'DEF456', 2);

Here is an example of reading employees using Javascript:

$(document).ready(function(){

    // show list of employees on first load
    showEmployees();

    $(document).on('click', '#select-company', function(){
        showEmployees();
    });

    $(document).on('click', '.order-by', function(){
        showEmployees();
    });
});

// function to show list of employees
function showEmployees(){
    var order_by = $('input[name=orderBy]:checked').val();
    if (typeof order_by === 'undefined' || !order_by) {
        order_by = "first_name";
    }

    var company_id = $("#select-company").val();
    if (typeof company_id === 'undefined' || !company_id) {
        company_id = "All";
    }

    var order_by_html = "";
    if (order_by === "first_name") {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='first_name' checked> First Name";
    } else {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='first_name'> First Name";
    }
    if (order_by === "last_name") {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='last_name' checked> Last Name";
    } else {
        order_by_html += "<input type='radio' class='order-by' name='orderBy' value='last_name'> Last Name";
    }

    // load list of companies
    $.getJSON("api/company/read.php", function(data) {
        // build companies option html
        // loop through returned list of data
        var companies_options_html = "";
        companies_options_html += "<select id='select-company' name='company_id' class='form-control pull-left'>";
        companies_options_html += "<option value='All'>All</option>";
        $.each(data, function (key, val) {
            if(val.id == company_id) {
                companies_options_html += "<option value='" + val.id + "' selected>" + val.name + "</option>";
            } else {
                companies_options_html += "<option value='" + val.id + "'>" + val.name + "</option>";
            }
        });
        companies_options_html += "</select>";

        // get list of employees from the API
        $.getJSON("api/employee/read.php?company_id=" + company_id + "&order_by=" + order_by, function(data){
            // html for listing employees
            read_employees_html = "";

            read_employees_html += "<div class='row'>";
                read_employees_html += "<div class='col-md-4'>" + companies_options_html + "</div>";

                read_employees_html += "<div class='col-md-4'>" + order_by_html + "</div>";

                // when clicked, it will load the create employee form
                read_employees_html += "<div class='col-md-4'><div id='create-employee' class='btn btn-primary pull-right create-employee-button' data-toggle='modal' data-target='#myModal'>";
                    read_employees_html += "<span class='glyphicon glyphicon-plus'></span> Create Employee";
                read_employees_html += "</div></div>";

            read_employees_html += "</div>";
            // start table
            read_employees_html += "<table class='table table-bordered table-hover'>";

                // creating our table heading
                read_employees_html += "<tr>";
                    read_employees_html += "<th class='w-25-pct'>First Name</th>";
                    read_employees_html += "<th class='w-10-pct'>Last Name</th>";
                    read_employees_html += "<th class='w-15-pct'>Company</th>";
                    read_employees_html += "<th class='w-25-pct text-align-center'>Action</th>";
                read_employees_html += "</tr>";

            // loop through returned list of data
            $.each(data, function(key, val) {

                // creating new table row per record
                read_employees_html += "<tr>";

                    read_employees_html += "<td>" + val.first_name + "</td>";
                    read_employees_html += "<td>" + val.last_name + "</td>";
                    read_employees_html += "<td>" + val.company.name + "</td>";

                    // 'action' buttons
                    read_employees_html += "<td>";
                        // read employee button
                        read_employees_html += "<button class='btn btn-primary read-one-employee-button' data-id='" + val.id + "' data-toggle='modal' data-target='#myModal'>";
                            read_employees_html += "<span class='glyphicon glyphicon-eye-open'></span> Read";
                        read_employees_html += "</button>";

                        // edit button
                        read_employees_html += "<button class='btn btn-info update-employee-button' data-id='" + val.id + "' data-toggle='modal' data-target='#myModal'>";
                            read_employees_html += "<span class='glyphicon glyphicon-edit'></span> Edit";
                        read_employees_html += "</button>";

                        // delete button
                        read_employees_html += "<button class='btn btn-danger delete-employee-button' data-id='" + val.id + "'>";
                            read_employees_html += "<span class='glyphicon glyphicon-remove'></span> Delete";
                        read_employees_html += "</button>";
                    read_employees_html += "</td>";

                read_employees_html += "</tr>";

            });

            // end table
            read_employees_html += "</table>";

            // inject to 'page-content' of our app
            $("#page-content").html(read_employees_html);

        });
    });
}

For more see here https://github.com/JonnyD/Technical-Test-Javascript-and-PHP/tree/master/app/employees

An example of a backend API:

<?php
// required header
header("Access-Control-Allow-Origin: *");
header("Content-Type: application/json; charset=UTF-8");

// include files
include_once '../config/database.php';
include_once '../objects/employee.php';
include_once '../objects/company.php';
include_once '../repository/employee_repository.php';
include_once '../repository/company_repository.php';

// instantiate database
$database = new Database();
$db = $database->getConnection();

// instantiate employee repository
$employeeRepository = new EmployeeRepository($db);

// instantiate company repository
$companyRepository = new CompanyRepository($db);

// get company ID
$companyId = isset($_GET['company_id']) ? $_GET['company_id'] : null;

// get order by
$orderBy = isset($_GET['order_by']) ? $_GET['order_by'] : null;

// query employees
if ($companyId != "All" && $companyId != null) {
    $employees = $employeeRepository->readByCompany($companyId, $orderBy);
} else {
    $employees = $employeeRepository->read($orderBy);
}

// check if more than 0 records found
if(count($employees) > 0) {
    $employeesArray = [];

    foreach ($employees as $employee) {
        $company = $companyRepository->readOne($employee->getCompanyId());

        $employeesArr[] = [
            'id' => $employee->getId(),
            'first_name' => $employee->getFirstName(),
            'last_name' => $employee->getLastName(),
            'job_title' => $employee->getJobTitle(),
            'email' => $employee->getEmail(),
            'phone' => $employee->getPhone(),
            'address_line_1' => $employee->getAddressLine1(),
            'address_line_2' => $employee->getAddressLine2(),
            'town_city' => $employee->getTownCity(),
            'county_region' => $employee->getCountyReqion(),
            'country' => $employee->getCountry(),
            'postcode' => $employee->getPostcode(),
            'company' => [
                'id' => $company->getId(),
                'name' => $company->getName()
            ]
        ];
    }

    header("HTTP/1.1 200");
    echo json_encode($employeesArr);
} else {
    header("HTTP/1.1 404");
    echo json_encode([
        "message" => "No employees found."
    ]);
}
?>

More here https://github.com/JonnyD/Technical-Test-Javascript-and-PHP/tree/master/api

EmployeeRepository:

<?php

class EmployeeRepository
{
    /**
     * @var string
     */
    private $tableName;

    /**
     * @var PDO
     */
    private $db;

    /**
     * @param PDO $db
     */
    public function __construct(PDO $db)
    {
        $this->tableName = "employee";
        $this->db = $db;
    }

    /**
     * @param string $orderBy
     * @return Employee[]
     */
    public function read(string $orderBy = null)
    {
        $query = "SELECT
                        id, 
                        first_name, 
                        last_name,
                        job_title,
                        email,
                        phone,
                        address_line_1,
                        address_line_2,
                        town_city,
                        county_region,
                        country,
                        postcode,
                        company_id
                    FROM
                        " . $this->tableName;

        if ($orderBy != null) {
            $query .= " ORDER BY " . $orderBy . " ASC";
        }

        $stmt = $this->db->prepare($query);
        $stmt->execute();

        $employees = [];
        if($stmt->rowCount() > 0) {
            // retrieve our table contents
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $employee = new Employee();
                $employee->setId($row['id']);
                $employee->setFirstName($row['first_name']);
                $employee->setLastName($row['last_name']);
                $employee->setJobTitle($row['job_title']);
                $employee->setEmail($row['email']);
                $employee->setPhone($row['phone']);
                $employee->setAddressLine1($row['address_line_1']);
                $employee->setAddressLine2($row['address_line_2']);
                $employee->setTownCity($row['town_city']);
                $employee->setCountyReqion($row['county_region']);
                $employee->setCountry($row['country']);
                $employee->setPostcode($row['postcode']);
                $employee->setCompanyId($row['company_id']);

                array_push($employees, $employee);
            }
        }

        return $employees;
    }

    /**
     * @param int $companyId
     * @param string $orderBy
     * @return Employee[]
     */
    public function readByCompany(int $companyId, string $orderBy = null)
    {
        $query = "SELECT
                        id, 
                        first_name, 
                        last_name,
                        job_title,
                        email,
                        phone,
                        address_line_1,
                        address_line_2,
                        town_city,
                        county_region,
                        country,
                        postcode,
                        company_id
                    FROM
                        " . $this->tableName . "
                    WHERE company_id = :companyId";

        if ($orderBy != null) {
            $query .= " ORDER BY " . $orderBy . " ASC";
        }

        $stmt = $this->db->prepare($query);
        $stmt->bindParam(":companyId", $companyId);
        $stmt->execute();

        $employees = [];
        if($stmt->rowCount() > 0) {
            // retrieve our table contents
            while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
                $employee = new Employee();
                $employee->setId($row['id']);
                $employee->setFirstName($row['first_name']);
                $employee->setLastName($row['last_name']);
                $employee->setJobTitle($row['job_title']);
                $employee->setEmail($row['email']);
                $employee->setPhone($row['phone']);
                $employee->setAddressLine1($row['address_line_1']);
                $employee->setAddressLine2($row['address_line_2']);
                $employee->setTownCity($row['town_city']);
                $employee->setCountyReqion($row['county_region']);
                $employee->setCountry($row['country']);
                $employee->setPostcode($row['postcode']);
                $employee->setCompanyId($row['company_id']);

                array_push($employees, $employee);
            }
        }

        return $employees;
    }

    /**
     * @param int $id
     * @return Employee|null
     */
    public function readOne(int $id)
    {
        $query = "SELECT 
                    id,
                    first_name,
                    last_name,
                    job_title,
                    email,
                    phone,
                    address_line_1,
                    address_line_2,
                    town_city,
                    county_region,
                    country,
                    postcode,
                    company_id
				FROM " . $this->tableName . "
				WHERE id = :id
				LIMIT 0,1";

        // prepare query statement
        $stmt = $this->db->prepare($query);

        // bind selected record id
        $stmt->bindParam(":id", $id);

        // execute the query
        $stmt->execute();

        // get record details
        $row = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($stmt->rowCount() > 0) {
            // assign values to object properties
            $employee = new Employee();
            $employee->setId($row['id']);
            $employee->setFirstName($row['first_name']);
            $employee->setLastName($row['last_name']);
            $employee->setJobTitle($row['job_title']);
            $employee->setEmail($row['email']);
            $employee->setPhone($row['phone']);
            $employee->setAddressLine1($row['address_line_1']);
            $employee->setAddressLine2($row['address_line_2']);
            $employee->setTownCity($row['town_city']);
            $employee->setCountyReqion($row['county_region']);
            $employee->setCountry($row['country']);
            $employee->setPostcode($row['postcode']);
            $employee->setCompanyId($row['company_id']);

            return $employee;
        } else {
            return null;
        }
    }

    /**
     * @param Employee $employee
     * @return bool
     */
    public function create(Employee $employee)
    {
        $query = "INSERT INTO
					" . $this->tableName . "
				SET
					first_name = :first_name, 
					last_name = :last_name,
					job_title = :job_title,
					email = :email,
					phone = :phone,
					address_line_1 = :address_line_1,
					address_line_2 = :address_line_2,
					town_city = :town_city,
					county_region = :county_region,
					country = :country,
					postcode = :postcode,
					company_id = :company_id";

        // prepare query
        $stmt = $this->db->prepare($query);

        // bind values
        $firstName = $employee->getFirstName();
        $stmt->bindParam(":first_name", $firstName);
        $lastName = $employee->getLastName();
        $stmt->bindParam(":last_name", $lastName);
        $jobTitle = $employee->getJobTitle();
        $stmt->bindParam(":job_title", $jobTitle);
        $email = $employee->getEmail();
        $stmt->bindParam(":email", $email);
        $phone = $employee->getPhone();
        $stmt->bindParam(":phone", $phone);
        $addressLine1 = $employee->getAddressLine1();
        $stmt->bindParam(":address_line_1", $addressLine1);
        $addressLine2 = $employee->getAddressLine2();
        $stmt->bindParam(":address_line_2", $addressLine2);
        $townCity = $employee->getTownCity();
        $stmt->bindParam(":town_city", $townCity);
        $countyRegion = $employee->getCountyReqion();
        $stmt->bindParam(":county_region", $countyRegion);
        $country = $employee->getCountry();
        $stmt->bindParam(":country", $country);
        $postcode = $employee->getPostcode();
        $stmt->bindParam(":postcode", $postcode);
        $companyId = $employee->getCompanyId();
        $stmt->bindParam(":company_id", $companyId);

        // execute query
        if ($stmt->execute()) {
            return true;
        } else {
            echo "<pre>";
            print_r($stmt->errorInfo());
            echo "</pre>";

            return false;
        }
    }

    /**
     * @param Employee $employee
     * @return bool
     */
    public function update(Employee $employee)
    {
        $query = "UPDATE
					" . $this->tableName . "
				SET
					first_name = :first_name, 
					last_name = :last_name,
					job_title = :job_title,
					email = :email,
					phone = :phone,
					address_line_1 = :address_line_1,
					address_line_2 = :address_line_2,
					town_city = :town_city,
					county_region = :county_region,
					country = :country,
					postcode = :postcode,
					company_id = :company_id
				WHERE
					id = :id";

        // prepare query statement
        $stmt = $this->db->prepare($query);

        // bind new values
        $id = $employee->getId();
        $stmt->bindParam(':id', $id);
        $firstName = $employee->getFirstName();
        $stmt->bindParam(":first_name", $firstName);
        $lastName = $employee->getLastName();
        $stmt->bindParam(":last_name", $lastName);
        $jobTitle = $employee->getJobTitle();
        $stmt->bindParam(":job_title", $jobTitle);
        $email = $employee->getEmail();
        $stmt->bindParam(":email", $email);
        $phone = $employee->getPhone();
        $stmt->bindParam(":phone", $phone);
        $addressLine1 = $employee->getAddressLine1();
        $stmt->bindParam(":address_line_1", $addressLine1);
        $addressLine2 = $employee->getAddressLine2();
        $stmt->bindParam(":address_line_2", $addressLine2);
        $townCity = $employee->getTownCity();
        $stmt->bindParam(":town_city", $townCity);
        $countyRegion = $employee->getCountyReqion();
        $stmt->bindParam(":county_region", $countyRegion);
        $country = $employee->getCountry();
        $stmt->bindParam(":country", $country);
        $postcode = $employee->getPostcode();
        $stmt->bindParam(":postcode", $postcode);
        $companyId = $employee->getCompanyId();
        $stmt->bindParam(":company_id", $companyId);

        // execute the query
        if ($stmt->execute()) {
            return true;
        }else{
            return false;
        }
    }

    /**
     * @param int $id
     * @return bool
     */
    public function delete(int $id)
    {
        $query = "DELETE FROM " . $this->tableName . " WHERE id = :id";

        // prepare query
        $stmt = $this->db->prepare($query);

        // bind id of record to delete
        $stmt->bindParam(':id', $id);

        // execute query
        if ($stmt->execute()) {
            return true;
        }

        return false;

    }
}
 

Example of an API

View more here https://github.com/JonnyD/Greetup-API-PHP/tree/master/src/GU

<?php

namespace GU\GangBundle\Controller\API;

use GU\BaseBundle\Controller\BaseController;
use GU\GangBundle\Entity\Gang;
use GU\GangBundle\Entity\GangUser;
use GU\GangBundle\Entity\JoinRequest;
use GU\GangBundle\Enum\Role;
use GU\GangBundle\Form\GangType;
use GU\GangBundle\Service\GangService;
use GU\GangBundle\Service\GangUserService;
use GU\GangBundle\Service\JoinRequestService;
use GU\GangBundle\Specification\CanViewGang;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use FOS\RestBundle\Controller\Annotations\Post;
use FOS\RestBundle\Controller\Annotations\Get;
use FOS\RestBundle\Controller\Annotations\QueryParam;

class GangController extends BaseController
{
    /**
     * @return Response
     */
    public function getGangsAction()
    {
        $gangService = $this->getGangService();
        $gangs = $gangService->getGangsWithinRadius(55.55555, 56.55555, 25);

        $canViewGangSpecification = $this->getCanViewGangSpecification();

        $gangsThatCanBeViewed = [];
        foreach ($gangs as $gang) {
            if ($canViewGangSpecification->isSatisfiedBy($gang)) {
                $gangsThatCanBeViewed[] = $gang;
            }
        }

        $response = $this->createApiResponse($gangsThatCanBeViewed);
        return $response;
    }

    /**
     * @param int $id
     * @return Response
     */
    public function getGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param Request $request
     * @return Response
     */
    public function postGangsAction(Request $request)
    {
        $data = json_decode($request->getContent(), true);

        $gang = new Gang();
        $form = $this->createForm(GangType::class, $gang);
        $form->submit($data);

        if ($form->isSubmitted()) {
            $gangService = $this->getGangService();
            $gangService->save($gang);

            $gangUser = new GangUser();
            $gangUser->setUser($this->getLoggedInUser());
            $gangUser->setGang($gang);
            $gangUser->setRole(Role::FOUNDER);

            $gangUserService = $this->getGangUserService();
            $gangUserService->save($gangUser);
        }

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param Request $request
     * @param int $id
     * @return Response|NotFoundHttpException
     */
    public function putGangAction(Request $request, int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $data = json_decode($request->getContent(), true);

        $form = $this->createForm(GangType::class, $gang);
        $form->submit($data);

        if ($form->isSubmitted()) {
            $gangService = $this->getGangService();
            $gangService->save($gang);
        }

        $response = $this->createApiResponse($gang);
        return $response;
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/join", name="join_gang")
     */
    public function joinGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $loggedInUser = $this->getLoggedInUser();

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($gang, $loggedInUser);

        if ($gangUser != null) {
            return $this->createNotFoundException("You are already a member");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestByGangAndUser($gang, $loggedInUser);

        if ($joinRequest != null) {
            return $this->createNotFoundException("You already requested to join this gang");
        }

        $joinRequest = new JoinRequest();
        $joinRequest->setGang($gang);
        $joinRequest->setUser($loggedInUser);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @param int $joinRequestId
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/accept-join-request/{join_request_id}", name="accept-join-request")
     */
    public function acceptJoinRequestAction(int $id, int $joinRequestId)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestById($joinRequestId);

        if ($joinRequest == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($joinRequest->getGang(), $joinRequest->getUser());

        if ($gangUser != null) {
            return $this->createNotFoundException("User is already a member");
        }

        $gangUser = new GangUser();
        $gangUser->setGang($joinRequest->getGang());
        $gangUser->setUser($joinRequest->getUser());
        $gangUser->setRole(Role::USER);

        $gangUserService->save($gangUser);
        $joinRequestService->remove($joinRequest);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @param int $joinRequestId
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/reject-join-request/{join_request_id}", name="reject-join-request")
     */
    public function rejectJoinRequestAction(int $id, int $joinRequestId)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $joinRequestService = $this->getJoinRequestService();
        $joinRequest = $joinRequestService->getJoinRequestById($joinRequestId);

        if ($joinRequest == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($joinRequest->getGang(), $joinRequest->getUser());

        if ($gangUser != null) {
            return $this->createNotFoundException("User is already a member");
        }

        $joinRequestService->remove($joinRequest);

        $response = $this->createApiResponse($joinRequest);
        return $response;
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @POST("/gangs/{id}/actions/leave", name="leave_gang")
     */
    public function leaveGangAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $loggedInUser = $this->getLoggedInUser();

        $gangUserService = $this->getGangUserService();
        $gangUser = $gangUserService->getGangUserByGangAndUser($gang, $loggedInUser);

        if ($gangUser == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService->remove($gangUser);

        return new Response(204);
    }

    /**
     * @param int $id
     * @return Response|NotFoundHttpException
     *
     * @GET("/gangs/{id}/actions/listMembers", name="list_members_gang")
     */
    public function listMembersAction(int $id)
    {
        $gangService = $this->getGangService();
        $gang = $gangService->getGangById($id);

        if ($gang == null) {
            return $this->createNotFoundException("Not found");
        }

        $gangUserService = $this->getGangUserService();
        $gangUsers = $gangUserService->getGangUsersByGang($gang);

        $response = $this->createApiResponse($gangUsers);
        return $response;
    }

    /**
     * @return GangUserService
     */
    private function getGangUserService()
    {
        return $this->get('gu.gang_user_service');
    }

    /**
     * @return GangService
     */
    private function getGangService()
    {
        return $this->get('gu.gang_service');
    }

    /**
     * @return JoinRequestService
     */
    private function getJoinRequestService()
    {
        return $this->get('gu.join_request_service');
    }

    /**
     * @return CanViewGang
     */
    private function getCanViewGangSpecification()
    {
        return $this->get('gu.can_view_gang_specification');
    }
}