Skip to content

seyDoggy/yo-angular-php-crud

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

yo-angular-php-crud

A sample Yeoman application with an integrated Slim PHP CRUD API.

Generate some data

It's assumed in this tutorial that you have a MySQL database called cars_demo with a table called cars, with 5 columns -- id, make, model, package, year. To give yourself something to work with, add at least one entry right now. Perhaps your favorite car, your dream car and the one you own today.

  1. Create the table:

    CREATE TABLE IF NOT EXISTS `cars` (
     `id` int(11) NOT NULL AUTO_INCREMENT,
     `make` varchar(50) NOT NULL,
     `model` varchar(50) NOT NULL,
     `pkg` varchar(50) NOT NULL,
     `year` varchar(50) NOT NULL,
     PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ;
  2. Insert some samples:

    INSERT INTO `cars` (`make`, `model`, `pkg`, `year`) VALUES
    ('Ford', 'Escape', 'xlt', '2009'),
    ('Porsche', '918', 'Spyder', '2013');

Setting up the app

  1. Read more about yo and AngularJS.

  2. Create your yo application (any will do, I just went with AngularJS).

    yo angular:app myWebApp
  3. Open the generated .gitignore and add the following after all the rest (we'll be using composer for our PHP dependencies):

    composer.phar
    vendor/
  4. Add a cars route:

    yo angular:route cars
  5. Because we're hosting a php api we're going to need a php server instead of the Connect node server that comes bundled with Yeoman. To do this we're going to install grunt-php:

    npm install --save-dev grunt-php
  6. Grunt-php (above) claims to be a drop in replacement for grunt-contrib-connect, so with that in mind, I just replaced all instances of the word connect in my Gruntfile.js with the word php and it works (for the most part).

  7. While in the Gruntfile.js, we'll add our soon-to-be created api folder to copy.dist.files[0].src:

    src: [
        '*.{ico,png,txt}',
        '.htaccess',
        
        ...
    
        'fonts/*',
        // add the api folder here
        'api/**'
    ]
  8. Also, at php.options, add the base folder for our future php script to run from:

      options: {
        port: 9000,
    
        ...
    
        // add the base folder to run php scripts from
        base:'<%= yeoman.app %>'
      },
  9. Now edit the .bowerrc file and change the bower components path to point to app/:

    {
      "directory": "app/bower_components"
    }
  10. And move the bower folder to the app folder:

    mv bower_components app/
  11. Run grunt serve to ensure the PHP server is working (be mindful of this issue)

  12. Go to http://localhost:9000/#/cars to check that the route renders.

  13. We'll come back to the front-end later.

Installing the Slim Framework

  1. Read more about Slim and composer.

  2. In the root of the app directory, make a directory called api:

    mkdir app/api
  3. Change over the api directory install composer locally (if it's not installed globally already):

    curl -s https://getcomposer.org/installer | php
  4. Make a new composer.json file and include the Slim dependency:

    {
        "require": {
            "slim/slim": "2.*"
        }
    }
  5. Install Slim via composer:

    php composer.phar install

Setting up the Slim PHP API

  1. In the app/api directory, make a new index.php file and add the following:

    <?php
    require 'vendor/autoload.php';

    This will allow all classes found in the vendor folder to be loaded automatically.

  2. Next we'll make a new instance of Slim:

    $app = new \Slim\Slim();
  3. Define some restful endpoints for dealing with cars using Slim's convenience methods for HTTP verbs, and run the slim application:

    $app->get('/cars', 'getCars');
    $app->get('/cars/:id', 'getCar');
    $app->post('/cars', 'addCar');
    $app->put('/cars/:id', 'updateCar');
    $app->delete('/cars/:id', 'deleteCar');
    
    $app->run();
  4. Define a connection function:

    function getConnection() {
        $dbhost="localhost";
        $dbport="8889";
        $dbuser="root";
        $dbpass="root";
        $dbname="cars_demo";
        $dbh = new PDO("mysql:host=$dbhost;dbname=$dbname", $dbuser, $dbpass);
        $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        return $dbh;
    }
  5. Define each of the restful functions:

    Create a car...

    function addCar() {
        $app = \Slim\Slim::getInstance();
        $req = $app->request();
        $car = json_decode($req->getBody());
        $sql = "INSERT INTO cars (make, model, year, pkg) VALUES (:make, :model, :year, :pkg)";
        try {
            $db = getConnection();
            $stmt = $db->prepare($sql);
            $stmt->bindParam("make", $car->make);
            $stmt->bindParam("model", $car->model);
            $stmt->bindParam("year", $car->year);
            $stmt->bindParam("pkg", $car->pkg);
            $stmt->execute();
            $car->id = $db->lastInsertId();
            $db = null;
            echo json_encode($car);
        } catch(PDOException $e) {
            echo '{"error":{"text":'. $e->getMessage() .'}}';
        }
    }

    Read all cars...

    function getCars() {
        $sql = "select * FROM cars ORDER BY id";
        try {
            $db = getConnection();
            $stmt = $db->query($sql);
            $cars = $stmt->fetchAll(PDO::FETCH_OBJ);
            $db = null;
            echo json_encode($cars);
        } catch(PDOException $e) {
            echo '{"error":{"text":'. $e->getMessage() .'}}';
        }
    }

    Read one car...

    function getCar($id) {
        $sql = "select * FROM cars WHERE id=".$id." ORDER BY id";
        try {
            $db = getConnection();
            $stmt = $db->query($sql);
            $cars = $stmt->fetchAll(PDO::FETCH_OBJ);
            $db = null;
            echo json_encode($cars);
        } catch(PDOException $e) {
            echo '{"error":{"text":'. $e->getMessage() .'}}';
        }
    }

    Update a car...

    function updateCar($id) {
        $app = \Slim\Slim::getInstance();
        $req = $app->request();
        $car = json_decode($req->getBody());
        $sql = "UPDATE cars SET make=:make, model=:model, year=:year, pkg=:pkg WHERE id=:id";
    
        try {
            $db = getConnection();
            $stmt = $db->prepare($sql);
            $stmt->bindParam("make", $car->make);
            $stmt->bindParam("model", $car->model);
            $stmt->bindParam("year", $car->year);
            $stmt->bindParam("pkg", $car->pkg);
            $stmt->bindParam("id", $id);
            $stmt->execute();
            $db = null;
            echo json_encode($car);
        } catch(PDOException $e) {
            echo '{"error":{"text":'. $e->getMessage() .'}}';
        }
    }

    Delete a car...

    function deleteCar($id) {
        $sql = "DELETE FROM cars WHERE id=:id";
        try {
            $db = getConnection();
            $stmt = $db->prepare($sql);
            $stmt->bindParam("id", $id);
            $stmt->execute();
            $db = null;
        } catch(PDOException $e) {
            echo '{"error":{"text":'. $e->getMessage() .'}}';
        }
    }
  6. Test that your api works. I use Postman.

Creating our Interface in AngularJS

Installing and applying Restangular

  1. To consume our restful endpoints I was going to create a factory that added some syntactic sugar to either $http or $resoure, but then I found Restangular and figured there was no point reinventing the reinvented wheel. We're going to use Restangular, which depends on lodash, so we need to include both in our project.

    Install...

    bower install restangular lodash --save

    ... and wire them into your app...

    grunt wiredep
  2. We need to include Restangular as a dependency in our app module. Edit your app/scripts/app.js and add 'restangular' to the list.:

    .module('yoAngularPhpCrudApp', [
        'ngAnimate',
        'ngCookies',
        ...
        'ngTouch',
        'restangular'
    ])
  3. We'll set a global configuration for Restangular in the same file, after our $routeProvider config. This isn't necessary, but we're making the assumption that all our restful calls will be made from the api route, therefore saving us a few bytes later on:

    .config(function (RestangularProvider) {
        RestangularProvider.setBaseUrl('/api/');
    })
  4. We now have the Restangular service available for injecting into our Angular objects. Let's inject Restangular into our cars controller. Edit app/scripts/controllers/cars.js:

    .controller('CarsCtrl', function ($scope, Restangular) {

Creating handlers in our Cars Controller

  1. Let's create our first Restangular object. In the body of our CarsCtrl, add the following:

    var url = 'cars';
    var Cars = Restangular.all(url);
  2. Next we'll add a blankCar object that will help us clear out our model from time to time.

    var blankCar = {
      make:"",
      model:"",
      pkg:"",
      year:""
    };
  3. And then a function expression that will get our initial data for us, using Restangular convenience methods, and serve to refresh our data with each digest cycle:

    var refreshCars = function() {
      $scope.isEditVisible = false;
      $scope.isAddNewCar = false;
      Cars.getList().then(function(data) {
          console.log('--> api/cars called from refreshCars()');
          $scope.cars = data;
      });
    };
  4. Now let's get to creating our $scope objects. We'll start with some function expressions that set a few flags for us to help set and determine various states:

    $scope.showAdd = function () {
      if ($scope.isEditVisible === false) {
          $scope.isAddNewCar = true;
      }
    };
    
    $scope.hideAdd = function () {
      $scope.isAddNewCar = false;
      $scope.newCar = angular.copy($scope.blankCar);
    };
    
    $scope.showEdit = function (idx) {
      $scope.isEditVisible = idx;
      $scope.newCar = angular.copy(blankCar);
    };
  5. Now we'll add the methods for our remaining HTTP verbs, post, put and delete. Again, we'll use Restangular awesomeness:

    $scope.saveCar = function() {
      $scope.isEditVisible = false;
      $scope.isAddNewCar = false;
      Cars.post($scope.newCar).then(function (data) {
          $scope.cars.push(data);
          console.log('--> newCar saved!');
      },
      function () {
          console.log('--> Could not save newCar!');
      });
    };
    
    $scope.updateCar = function(idx) {
      $scope.isAddNewCar = false;
      Cars.getList().then(function(data) {
          console.log('--> api/cars called from updateCar()');
          $scope.cars = data;
          var carWithId = _.find($scope.cars, function(car) {
              return car.id === $scope.cars[idx].id;
          });
    
          carWithId.make = $scope.newCar.make || carWithId.make;
          carWithId.model = $scope.newCar.model || carWithId.model;
          carWithId.pkg = $scope.newCar.pkg || carWithId.pkg;
          carWithId.year = $scope.newCar.year || carWithId.year;
          carWithId.put();
          $scope.isEditVisible = false;
      });
    };
    
    $scope.deleteCar = function(idx) {
      var car = Restangular.one(url, $scope.cars[idx].id);
      car.remove();
      $scope.cars.splice(idx, 1);
    };
  6. Finally, we'll run our refreshCars() method:

    refreshCars();

Creating our View

I'm not going to go into a whole lot of detail here about the various things that are happening, but I will point out a few of the basic ideas.

In addition to adding new cars within the view, we're employing an edit-in-place methodology so the cars we do create can be edited within that same view as well. This will allow for a better user experience than shuffling the user off to different views to add and edit entries.

However, there are a lot of things to consider when using one view for multiple tasks, namely preventing users from being able to add a new car and edit another one at the same time. Or preventing users from performing actions on any of the other entries while editing another.

So as you read through the code, have a look at some of the directives in use and some of the logic used to show and hide various components and disable others, depending on the current actions being performed.

<div class="carsContainer">
    <button ng-click="showAdd()" type="button" class="btn btn-sm btn-primary" ng-disabled="isAddNewCar || isEditVisible !== false" aria-hidden="true"><i class="glyphicon glyphicon-plus"></i> Add Car</button>
    <form novalidate="novalidate" class="form-horizontal">
        <table class="table">
            <thead>
                <tr>
                    <th>#</th>
                    <th>Make</th>
                    <th>Model</th>
                    <th>pkg</th>
                    <th>Year</th>
                    <th class="text-center">
                        <span ng-hide="isAddNewCar || isEditVisible !== false">Delete</span>
                        <span ng-show="isAddNewCar || isEditVisible !== false">Cancel</span>
                    </th>
                    <th class="text-center">
                        <span ng-hide="isAddNewCar || isEditVisible !== false">Edit</span>
                        <span ng-show="isAddNewCar || isEditVisible !== false">Save</span>
                    </th>
                </tr>
            </thead>
            <tbody>
                <tr ng-show="isAddNewCar">
                    <td>#</td>
                    <td><input ng-model="newCar.make" type="text" class="form-control"/></td>
                    <td><input ng-model="newCar.model" type="text" class="form-control"/></td>
                    <td><input ng-model="newCar.pkg" type="text" class="form-control"/></td>
                    <td><input ng-model="newCar.year" type="text" class="form-control"/></td>
                    <td class="text-center">
                        <button ng-click="hideAdd()" type="button" class="btn btn-sm btn-warning" aria-hidden="true">
                            <i class="glyphicon glyphicon-ban-circle"></i>
                        </button>
                    </td>
                    <td class="text-center">
                        <button ng-click="saveCar()" type="submit" class="btn btn-sm btn-success" aria-hidden="true">
                            <i class="glyphicon glyphicon-ok"></i>
                        </button>
                    </td>
                </tr>
                <tr ng-repeat="car in cars">
                    <td>{{$index + 1}}</td>
                    <td>
                        <span ng-hide="isEditVisible === $index">{{car.make}}</span>
                        <input ng-show="isEditVisible === $index" ng-model="newCar.make" type="text" class="form-control" ng-value="{{car.make}}" placeholder="{{car.make}}"/>
                    </td>
                    <td>
                        <span ng-hide="isEditVisible === $index">{{car.model}}</span>
                        <input ng-show="isEditVisible === $index" ng-model="newCar.model" type="text" class="form-control" ng-value="{{car.model}}" placeholder={{car.model}}/>
                    </td>
                    <td>
                        <span ng-hide="isEditVisible === $index">{{car.pkg}}</span>
                        <input ng-show="isEditVisible === $index" ng-model="newCar.pkg" type="text" class="form-control" ng-value="{{car.pkg}}" placeholder="{{car.pkg}}"/>
                    </td>
                    <td>
                        <span ng-hide="isEditVisible === $index">{{car.year}}</span>
                        <input ng-show="isEditVisible === $index" ng-model="newCar.year" type="text" class="form-control" ng-value="{{car.year}}" placeholder="{{car.year}}"/>
                    </td>
                    <td class="text-center">
                        <button ng-hide="isEditVisible === $index" ng-click="deleteCar($index)" type="button" ng-disabled="isAddNewCar || isEditVisible !== false" class="btn btn-sm btn-link">
                            <i class="glyphicon glyphicon-trash"></i>
                        </button>
                        <button ng-show="isEditVisible === $index" ng-click="showEdit(false)" type="button" class="btn btn-sm btn-warning" aria-hidden="true">
                            <i class="glyphicon glyphicon-ban-circle"></i>
                        </button>
                    </td>
                    <td class="text-center">
                        <button ng-hide="isEditVisible === $index" ng-click="showEdit($index)" type="button" ng-disabled="isAddNewCar" class="btn btn-sm btn-link">
                            <i class="glyphicon glyphicon-pencil"></i>
                        </button>
                        <button ng-show="isEditVisible === $index" ng-click="updateCar($index)" type="submit" class="btn btn-sm btn-success" aria-hidden="true">
                            <i class="glyphicon glyphicon-ok"></i>
                        </button>
                    </td>
                </tr>
            </tbody>
        </table>
    </form>
</div>

About

A sample Yeoman application with an integrated Slim PHP CRUD API

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published