Jump to content

How best to implement slim with OOP


NotionCommotion

Recommended Posts

Trying to improve my OOP skills, and revamping how I have previously implemented slim APIs.  Any general remarks plus responses for the below specific items would be appreciated.

  1. See any/many problems with my implementation?  I still don't know if I have function in the right locations, and I was fishing for this in my https://forums.phpfreaks.com/topic/306502-chicken-or-the-egg-conundrum-when-working-with-entities/ post.
  2. How should errors be communicated from my individual classes back to slim?  I've tried multiple implementations, however, have not totally decided.
    • Exceptions.  Given the many negative sediments for doing so, I have to believe it is not a good idea, and have stopped doing so.
    • Have my class methods return something like [0,error message] and [1, success message].  Seems a little clunky.
    • Have my objects set some error property, have the method return true/false, and then if false, get the error.  I brought this approach up several days ago, and requinx felt is wasn't the best.
    • Have my class method return [content,httpCode].  A little clunky.
    • Put more validation in the main slim file and have some of my classes return an error array as I am doing below.  Don't know if I like it.
    • Pass the response to my classes and have them update it.  I am starting to think that this is the "right" way to do it, but I haven't seen it done which makes me suspect.
  3. How should my validation class be provided to the entity?  Injected, inheritance, as a trait, etc?
// ##### WEBPAGE VIEW RESPONSES #####

$app->get('/users', function (Request $request, Response $response) {
    return $this->view->render($response, 'users.html',[
        'users'=>$this->router->pathFor('users'),
        'menu'=>(new Html())->getMenu('/users')
    ]);
});

// ##### API RESPONSES #####
define('_VER_','/api/1.0');

$app->get(_VER_.'/users', function (Request $request, Response $response) {
    return $response->withJson((new UserMapper($this->get('pdo')))->get());
})->setName('users');

$app->post(_VER_.'/users', function (Request $request, Response $response) {
    $userMapper = new UserMapper($this->get('pdo'));
    $user = new User($request->getParsedBody());
    if($errors=$user->validate()) {
        return $response->withJson($errors,422);
    }
    else {
        $userProperties=$userMapper->create($user);
        return $response->withJson($userProperties);
    }
});

$app->get(_VER_.'/users/{id}', function (Request $request, Response $response, $arg) {
    return $user = (new UserMapper($this->get('pdo')))->getById($arg['id'])
    ?$response->withJson($user->getProperties())
    :$response->withJson(["User with id $arg[id] does not exist"],422);
});

$app->put(_VER_.'/users/{id}', function (Request $request, Response $response, $arg) {
    $userMapper = new UserMapper($this->get('pdo'));
    if($user = $userMapper->getById($arg['id'])) {
        //Or maybe use $user->validate() first and then have $user->update() return true/false?
        if($errors=$user->update($request->getParsedBody())) {
            return $response->withJson($errors,422);
        }
        else {
            $userProperties=$userMapper->update($user);
            return $response->withJson($userProperties);
        }
    }
    else $response->withJson(["User with id $arg[id] does not exist"],422);
});

$app->delete(_VER_.'/users/{id}', function (Request $request, Response $response, $arg) {
    $userMapper = new UserMapper($this->get('pdo'));
    if($user = $userMapper->getById($arg['id'])) {
        return $errors = $userMapper->delete($user)
        ?$response->withJson($errors,422)
        :$response->withJson($user->getProperties());
    }
    else $response->withJson(["User with id $arg[id] does not exist"],422);
});
interface EntityMapperInterface
{
    public function get();
    public function create(User $user);
    public function update(User $user);
    public function delete(User $user);
    public function getById($id);
}
class UserMapper extends EntityMapper implements EntityMapperInterface
{

    public function create(User $user)
    {
        $userProperties=$user->getProperties();
        $userProperties->password=password_hash($userProperties->password, PASSWORD_DEFAULT);
        $stmt=$this->pdo->execute($this->pdo('INSERT INTO users(id, username, role, password) VALUES (null, :username, :role, :password)'));
        if($stmt->execute((array) $userProperties)) {
            unset($userProperties->password);
            $userProperties->id=$this->pdo->lastInsertId();
            return $userProperties;
        }
        else throw new EntityMapperException('Unknown error creating User');
    }

    public function update(User $user)
    {
        //Not complete
    }
    public function delete(User $user)
    {
        $stmt=$this->pdo->prepare('DELETE FROM users WHERE id=?');
        try {
            $stmt->execute([$user->id]);
            return true;
        } catch(\PDOException $e){
            throw New EntityMapperException('Foreign key error?');
        }
    }

    public function getByUserName($name)
    {
        $stmt=$this->pdo->prepare("$this->getQuery() WHERE u.username=?");
        $stmt->execute([$name]);
        return $stmt->fetch();
    }

    protected function getQuery()
    {
        return 'SELECT u.id, u.username, u.role, u.password, ur.access_level FROM users u INNER JOIN user_role ur ON ur.role=u.role';
    }
}
abstract class EntityMapper
{

    protected $pdo;

    public function __construct(\PDO $pdo)
    {
        $this->pdo=$pdo;
    }

    public function get()
    {
        $stmt=$this->pdo->query($this->getQuery());
        return $stmt->fetchAll();
    }

    public function getById($id)
    {
        $stmt=$this->pdo->prepare("$this->getQuery() WHERE u.id=?");
        $stmt->execute([$id]);
        return $stmt->fetch();
    }

    abstract protected function getQuery();

}
interface EntityInterface
{
    public function update($data);
    public function validate();
    public function getProperties();
}
class User extends Entity implements EntityInterface
{

    protected $firstname,
    $lastname,
    $email,
    $role,
    $accessLevel,
    $validator;

    public function __construct($properties)
    {
        //Should Validator be injected, should this class should be extended by it, should it be a trait, or should some other means be used?
        $this->validator=new Validator([
            'firstname'=>"string",
            'lastname'=>"string",
            'email'=>"string:email",
            'role'=>"string:[user,admin]",
            'accessLevel'=>"integer:betweenValue,1,10",
        ]);
        foreach($this->validator->iterate($properties) as $name=>$value) {
            $this->$name=$value;
        }
    }
}
Abstract class Entity
{
    public function update($data)
    {
        foreach($this->validator->iterate($properties) as $name=>$value) {
            $this->$name=$value;
        }
        return $this->validate();
    }


    public function validate()
    {
        return $this->validator->validate($properties);
    }


    public function getProperties()
    {
        return $this->validator->getProperties();
    }
}

 

Link to comment
Share on other sites

You should use exceptions in your model. To what this translates to is up to the application.

 

The entity should not contain the logic for validation. A separate object should do this.

 

The same goes for getProperties() simply create the array from the getters.

 

Also _VER_ does not work. What happens to anyone using /api/1.0 when you set _VER_ to /api/2.0?

Link to comment
Share on other sites

Thank you ignace,

 

Regarding exceptions, would you consider my UserMapper the model?  Would you frown on exceptions being used to convey state of user input?

 

I expected to either create new endpoints with new resources to deal with /api/2.0, or have the new endpoint access the same resource if applicable, however, I haven't given it too much thought.  Another option is regex, but again haven't thought much about it.  Is there a typical approach for this?

 

Regarding using a separate object for validation and getting the properties and using DI where possible, I think this is finally starting to set in with me partially based on your and requnix's replies to my https://forums.phpfreaks.com/topic/306502-chicken-or-the-egg-conundrum-when-working-with-entities/ post.  Let me give an example of how I previously did things and how I think I will be doing more often in the future.  Say I was making a website builder, and had pages which displayed a list of pages, a list of menus, a list of users, a list of images, or whatever.  Just things that are not really the same, but have common actions performed on them such as CRUD and maybe others such as changing permissions, etc.

 

In the past, I would create an abstract list class, and then extend that class using PagesList, MenusList, UsersList, etc.  Sometimes, this worked well, other times, not so much.

 

Now I am thinking I should sometimes just create a single List class which they all directly use, but will pass its constructor a PagesMapper, MenusMapper, UsersMapper object to make it specific (should I be referring to this object as something different than "mapper"?).  Then I could have common code to say list all the entities, but this object would act differently to specifically performed the desired scope.

 

So, for validation, I would pass some more generic class a UserValidator object (which is probably extended from Validator), and that object would be fully accountable to perform all validation needs?

 

 

I hope this is not completely off base on what you are recommending...

Link to comment
Share on other sites

I think you are caught in a loop. Most programmers fall pray to this, because all guru's recommend and promote the use of heavily marketed patterns, brainwashing anyone who reads their articles, so that anyone who doesn't use them feel out of the loop or stupid.

 

They are correct to use these patterns and their carefully crafted examples have a certain truth to them. Don't get too caught up in naming and adhere to programming principles more than naming. 

  1. Try to keep related things together. Like querying (CRUD) users from a DB. Sending out e-mails. 
  2. Don't think "in the future i might need". You don't.
  3. Don't be afraid to create "microscopic" classes like RegisterUser or SendRegistrationMail or whatever you can think up. Make sure it does only 1 thing.

I am speaking from experience here. I was caught in that same loop always trying to use patterns whenever I could without fully understanding them, I still don't understand most of them nor do I desire to do so. I let my code do the talking.

<?php

// Here's an example using the Action-Domain-Responder from Slim
// https://www.slimframework.com/docs/cookbook/action-domain-responder.html

$api->put(_VER_ . '/users', function(Request $request, Response $response) {
  try {
  
    $newUser = $this->apiUserService->create($request->getParsedBody());
  
    return $this->apiUserResponder->wasCreated($newUser); // 201
  
  } catch (PermissionDeniedError $e) {
    return $this->apiUserResponder->notAllowed(); // 401
  
  } catch (UserValidationError $e) {
    return $this->apiUserResponder->containsErrors($e);
  }
});

Simple to understand, easy to maintain. The ApiUserService does all the model related work for users, CRUD, checking permissions, validating, .. it delegates the actual task mind you to the appropriate class.

 

The Service class is merely a Facade (in guru speak).

Link to comment
Share on other sites

*gasp*  You are using exceptions to valid user input....  The horror! The horror!

 

That being said, your code is very simple and easy to understand especially by using descriptive exceptions such as PermissionDeniedError and UserValidationError, and provides intuitive multiple dimension error feedback which solves all of my "how should errors be communicated" dilemmas.  I should not have bowed to pubic pressure that exceptions should only be used for "exceptional" behavior and never, ever user for user input, bla, bla, bla....

 

I am not instantly flip-flopping on my newly rekindled belief that exceptions can be used to communicate user behavior, but perhaps a cleaner Slim specific implementation might also use the $response to communicate state and look like the following:

 




$container['userMapper'] = function($c) {
    return new UserMapper($c['db']);
};

$container['userValidator'] = function($c) {
    return new UserValidator();
};

$container['apiUserService'] = function($c) {
    return new ApiUserService($this->userMapper, $this->userValidator);
};

$app->group('', function () use ($app) {

    //All endpoints within this group are limited to administrators only

    $api->post('/users', function(Request $request, Response $response) {
        return $this->apiUserService->create($request->getParsedBody(), $response);
    });

})->add(new Authenticator($container->get('settings')['jwt'], 'admin'));




class ApiUserService extends ApiService
{
    public function __construct(Mapper $userMapper, Validator $userValidator)
    {
        $this->mapper=$userMapper;
        $this->validator=$userValidator;
        /* maybe also include the response? */
    }

    public function create($dirtyUserInput, Response $response)
    {
        try {

            $cleanUserInput = $this->validator->sanitize($dirtyUserInput);
            $this->validator->validate($cleanUserInput);
            $user=$this->mapper->create($cleanUserInput);
            return $response->write($user)->withStatus(201);

        } catch (UserValidationError $e) {
            return $response->write($this->getStdErrorMsg(422))->withStatus(422);

        } catch (SomeOtherError $e) {
            return $response->write('as applicable')->withStatus(422);
        }

    }
}



Maybe/probably not a big deal, but $response is immutable so it uses a little more memory.  One part I am not sure about is whether the response should be passed to the create() method or the constructor.

 

While I agree that we all can get a little OCD about attempting to utilize some of these worshiped design patterns, one which I do think I need to better understand and utilize is how passing an object to a class can be used to modify the classes behavior.  On my previous post, I was attempting to inquire about this concept.  Before going too far down this rabbit hole, I would like some confirmation that it can end up somewhere good, and would appreciate any thoughts.

 

Thanks!

Link to comment
Share on other sites

I would not pass any application specifics to the model.
 

<?php

// assume
interface UserMapper {}
class ApiUserMapper implements UserMapper {
  ..
}

class ApiUserService {
  public function __construct(UserMapper $userMapper, Validator $validator) {
    // ..
  }
  
  public function createUser(array $userData): User {
    if (!$this->validator->isValid($userData)) {
      throw new UserValidationError($this->validator, $userData);
    }

    return $this->userMapper->add($userData);
  }
}

The comment about not using exceptions for validation has some truth to it.

 

However the data passed must be valid for your model to do any work. It's possible to extract out the validation into a Form component and do a isSubmitted() && isValid() before calling your service but this means that EVERYWHERE in your application you first need to call the Form component before being able to call your service. And I'm far too lazy to add all these dependencies and make all those calls EVERYWHERE.

 

I use a Service because it's convenient, it does everything for me. No matter if that be in an API, Web, or CLI.

Link to comment
Share on other sites

I suppose your comment about not passing ss any application specifics to the model makes sense.

 

Don't start getting me worried again about exceptions and validation!

 

I didn't previously pay attention to your remarks about it being a Service.  A "Service" is just a class/object with a common interface available to whatever needs to use it?

Link to comment
Share on other sites

*gasp* You are using exceptions to valid user input.... The horror! The horror!

The main "problem" I have with when people use exceptions to validate user input is them throwing an exception for each validation step. For example:

public function validate(){
    if (empty($this->firstName)){
        throw new ValidationException('First name is required');
    }

    if (empty($this->lastName)){
        throw new ValidationException('Last name is required');
    }

    if (empty($this->email)){
        throw new ValidationException('Email is required');
    } else if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)){
        throw new ValidationException('Email must be a valid email address.');
    }

    //...
}
That makes for a terrible experience because if there are multiple errors you only get the know about them one at a time.

 

If you're going to use exceptions for validating user input, at least do it in a way that lets you get all the error information at once.

public function validate(){
    $Errors = [];
    if (empty($this->firstName)){
        $Errors[] = 'First name is required';
    }

    if (empty($this->lastName)){
        $Errors[] = 'Last name is required';
    }

    if (empty($this->email)){
        $Errors[] = 'Email is required';
    } else if (!filter_var($this->email, FILTER_VALIDATE_EMAIL)){
        $Errors[] = 'Email must be a valid email address.';
    }

    if ($Errors){
        throw new ValidationException($Errors);
    }
}

As far as your overall validation / entity management implementations, I'd refer you to Symfony Validator and Doctrine ORM for inspriation as they seem to match up well to what you are wanting to do.

Link to comment
Share on other sites

I didn't previously pay attention to your remarks about it being a Service.  A "Service" is just a class/object with a common interface available to whatever needs to use it?

 

A service is a direct answer to a question. It encapsulates all logic usually found in a controller so that this logic becomes reusable in other parts of the application. And that's the main reason I use services.

 

Now to what I mean with it being an answer to a question. Like the examples I gave above it could create a new user directly from the input. The same goes for getting, updating, and deleting.

<?php

interface UserService {
  public function get(int $id): ?User;
  public function create(array $data): User
  public function update(array $data, int $id): User
  public function delete(int $id): void;
}

It's a simplified interface which doesn't require any previous querying. If you apply CQRS, you would have (notice how all methods on UserCommandService return void):

<?php

interface UserQueryService {
  public function get(int $id): User
}

interface UserCommandService {
  public function create(array $data): void;
  public function update(array $data, int $id): void;
  public function delete(int $id): void;
}
Link to comment
Share on other sites

Gotcha!

 

Also, just learned two new items:

  1. http://php.net/manual/en/functions.returning-values.php#functions.returning-values.type-declaration
  2. https://en.wikipedia.org/wiki/Command%E2%80%93query_separation

What is the purpose of the question mark?  Assuming not a typo, where is it documented in the manual?  If I were to guess, it indicates User or NULL.  Thanks

public function get(int $id): ?User;

EDIT.  Would you ever consider catching all exceptions in the Slim handler instead of for each route?

$api->put(_VER_ . '/users', function(Request $request, Response $response) {
try {

$newUser = $this->apiUserService->create($request->getParsedBody());

return $this->apiUserResponder->wasCreated($newUser); // 201

} catch (PermissionDeniedError $e) {
return $this->apiUserResponder->notAllowed(); // 401

} catch (UserValidationError $e) {
return $this->apiUserResponder->containsErrors($e);
}
Link to comment
Share on other sites

What is the purpose of the question mark?  Assuming not a typo, where is it documented in the manual?  If I were to guess, it indicates User or NULL.  Thanks

As you guessed, the ? means it can be null. Seems the manual hasn't been updated with that info yet, though a user note mentions it.

 

EDIT.  Would you ever consider catching all exceptions in the Slim handler instead of for each route?

If you can handle an exception, you should catch it at whatever level is appropriate. For example if a PermissionDeniedException is thrown then you'd likely handle that the same way regardless of the current route, so it makes more sense to catch it at a higher level rather than repeat the code over and over.

 

If you handle a UserValidationError exception the same for every route then you'd want to bump it up a level also, otherwise keep it at a route level so you can handle it in a manner appropriate for that route.

Link to comment
Share on other sites

  • 2 months later...

Thanks ignace,

 

I read one post that describes one approach and I start implementing, but then read another which does differently, and find myself going in circles.  I am sold on using a service, so at least that is one thing constant.  I am looking for one complete example that reflects good practice so I may model my own work on it and stop second guessing myself.  Would you recommend using  one of the two tutorials on the Slim site, or some other?  Note that I am not looking for Slim specific direction, but overall good and practical OOP direction.

 

Regarding the ones on the Slim site, first, I was using https://github.com/slimphp/Tutorial-First-Application/tree/master/src, but it doesn't use a service.  You previously referenced https://www.slimframework.com/docs/v3/cookbook/action-domain-responder.html, but unlike the "first tutorial", I couldn't find the full code for this one on github, but only https://github.com/slimphp/Slim-Website/blob/gh-pages/docs/v3/cookbook/action-domain-responder.md.  Are the entity classes use for the first tutorial still applicable?  Is this "action-domain-response" applicable or is just some other flavor of the month?

 

Also, I don't know if it matters, but I currently am not using Doctrine or any 3rd party ORM.  I don't know whether warranted, but I am a little hesitant to try.  Do you think it is important to use one?

 

Thanks!

 

Link to comment
Share on other sites

Also, specifically, I am working on a charting app which deals with bar, line, and pie charts, etc. Each of these also could have multiple categories and series.  I am assuming that each of these will have their own mapper and entity classes, right?  I am not really sure about whether the categories and series should also have their own service class.  I have an endpoint to clone a chart which would require action on a chart and its categories and series, and it seems convoluted that a service would call another service, but maybe it is okay.

 

If they are handled by the chart service, I am also uncertain whether the chart service should receive their mappers directly in its constructor so that it could directly access them, or whether the categories and series mapper should be injected into the chart mapper, and then the chart service can either access them via a chart mapper method or maybe even access these sub-mappers methods directly.

 

And around and around I go....

Link to comment
Share on other sites

but then read another which does differently, and find myself going in circles

Been there, done that! The real problem is the second-guessing. You think of yourself as a not good enough programmer, so you try to learn by looking at examples. But every example that you look at uses different methods, new ideas, different approaches.

 

Which is how you end up running in circles. It takes a lot of practice, and years of experience until one day you realise that all that really matters are the basic principles of OOP:

 

- S.O.L.I.D.

- Prefer composition over inheritance

 

And this idea is so simple, everyone can do it, and distinguish good from bad.

 

To come back to your questions, using an ORM makes life much easier unless your inheritance graph is very simple (no children). When it is, you can choose to do the mapping yourself. Bar, line, pie charts are all views, not individual models (entities).

Create a model (entity) that captures the data, and then let the view handle how it will render that data (bar, line, pie, ..).

Link to comment
Share on other sites

Thanks ignace,

 

Yesterday was a horrible day and reminded me of when I was less experienced in relational database design and kept on changing my schema.

 

Is there any ORMs you would recommended?  I would rather not "try them all and make my own decision" as I expect each has a fairly high learning curve.  I know Doctrine is popular but I've also read that it might be on the decline so maybe it doesn't make sense for me to chose that one.

 

Without formal training, it is difficult to know what to use as a good practice OOP authority.  The PHP manual is great for implementation but not for concepts.

 

Typically, I've read that the basic principles of OOP are encapsulation, inheritance, and polymorphism.  I've discovered that forcing inheritance can become very complicated, and now see what you mean by prefer composition over inheritance.

 

 

I don't think I've ever read about SOLID before, but have since read https://scotch.io/bar-talk/s-o-l-i-d-the-first-five-principles-of-object-oriented-design and some wikipedia posts.  Below are my notes, and would appreciate any corrections should there need any.

 

S - Single-responsibility principle.  Pretty self-explanatory.  Example: Don't have a single class be responsible to calculate the area and format the results.

 

O - Open-closed principle.  Ability to extend a class without modifying it, and use an interface to enforce use.  Example: Put the area calculation in the individual object entities and not in some calculator class using if/then logic.

 

L - Liskov substitution principle.  Objects in a program should be replaceable with instances of their subtypes.  Example: Each of these classes' constructors must require the same input type and common methods must return the same output type.

 

I - Interface segregation principle.  Don't apply an interface to a class with methods not used by that class, but instead put common methods in a separate interface with common method names.  What is the "client" as referred to by "A client should never be forced to implement an interface..."?

 

D - Dependency Inversion Principle.  An abstraction layer must exist between high level and low level modules/entities, and they should not be dependent on final/concrete objects. Example: Have interface enforce a DBConnectionInterface::connection and not a MySQLConnection::connection().

 

I've definitely not fully followed most of these principles in the past, and clearly see the precarious position I am by not doing so.

 

Thanks for pointing out SOLID.

Link to comment
Share on other sites

I am actually getting this!

 

I've always just queried the DB, returned the results to the router, and had the router pass them to the view, and never first injected the DB results in an entity object as shown in the Slim tutorials.  Should I be?

 

The following represents my understanding of the desired work flow.  Please comment if inaccurate (especial the update and delete requests which are not described in the Slim tutorials).

 

1. Router (i.e. Slim) invokes appropriate service method based on action (endpoint).

2. Service object is injected using pimple's DI container with applicable mappers (not entities, right?)

3. Service validates/sanitizes user data (if applicable), and throws an exception if applicable.  Exceptions can either be caught by using the Slim exception handler (which I will likely do) or in each endpoint.

4. Service will request an entity (or an array of entities) from the appropriate mapper method (likely not be necessary for create/update?/delete requests).

     - Mapper will query database and inject the results into an Entity class and return the resulting object to the service.  For collections, mapper will query the database and inject each record into the Entity class, and return an array of entities to the service.

 

READ REQUESTS

5. N/A

6. N/A

 

CREATE REQUESTS

5. User data (and potentially DB data received in step #4) will be sent to an entity classes constructor resulting in a new object.

6. Service will send the object to the applicable mapper method to be saved.

a. The mapper method will attempt to save the data, and if unable, throw an exception which will be caught by Slim's exception handler (and not a try/catch block in the service).  I will likely not use PHP to validate unique constraints but leave this to the DB.  Alternatively, it could return true/false.  Any thoughts?

b. The mapper doesn't return anything (therefore false is returned by default).

 

DELETE REQUESTS

5. Service will send the ID to the applicable mapper method to be deleted.

a. The mapper method will attempt to delete the record, and if unable, throw an exception (or return true/false similar to what is dong for a create requests #6a).  Most likely reasons for failure will be the ID doesn't exist (detected using rowCount) or a foreign key constraint (will automatically throw and exception).  I am assuming that the entity should not be responsible to delete itself (please confirm).

b. The mapper doesn't return anything (therefore false is returned by default).

6. N/A

 

UPDATE REQUESTS

5. Service will send the ID and applicable data(associated array) to the applicable mapper method to be updated.

a. The mapper method will attempt to update the record, and if unable, throw an exception (and other actions similar to DELETE).

b. The mapper doesn't return anything (therefore false is returned by default).

6. N/A

 

ALL CRUD REQUESTS

7. The service return the entity object (or the array of objects) to the router.

8. The router sends the object(s) received to the applicable responder method.

9. The responder acts accordingly.

 

Link to comment
Share on other sites

Throwing in my $.02 - as far as thorough, understandable explanation of OOP as it specifically relates to PHP I've found this book to be a great resource - https://www.amazon.com/Objects-Patterns-Practice-MATT-ZANDSTRA/dp/1484219953/ref=sr_1_1?ie=UTF8&qid=1526221870&sr=8-1&keywords=php+objects+patterns+and+practice.

 

Some of the earlier chapters may be a bit basic for you given where you stand and the research you've done, but I've actually bought the second, third, and fourth editions (the fifth isn't out in B&N e-book format yet, as far as I can tell) and to this day just pick it up to read on a random basis.

 

I know this doesn't specifically address any of the questions you've laid out above, but if you're looking for an enjoyable read that's also informative, it's worth taking a look.

Link to comment
Share on other sites

I prefer to use Doctrine since it does not force your models to extend any of their classes to work (ie AbstractModel). Nor does it force you to use an ActiveRecord pattern (eg $entity->save() thus binding your DB to your model).

 

After reading your responses, you are still overthinking it. Keep it simple. Experienced OOP programmers are not part of some secret illuminati group that have some special knowledge that they share over encrypted networks.

 

If you don't understand SOLID from the get-go, then don't spend any time figuring it out. You will understand it, with experience. That's the mistake I made, I would write 100th lines of code and would then throw it out and start over. Because I wasn't satisfied with the result. Achieving nothing in the process.

 

It should be easy. It should be logical. That's it.

 

If I am tasked to change something in the registration process. It should not take me more than 2 seconds to figure out where I need to make the changes.

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.