0

I have a set of objects (MainObject) which are uniquely defined by two objects (SubObject1, SubObject2) and a string (theString). I with to retrieve a MainObject from the set by returning an existing object based on the two subobjects and string should it exist, else creating a new one, adding it to the set, and returning that object.

The following pseudo code demonstrates this in the make believe world where a standard array can use objects as keys.

class SubObject1{}
class SubObject2{}
class MainObject{
    private $subObject1, $subObject2, $theString;
    public function __construct(SubObject1 $subObject1, SubObject2 $subObject2, string $theString):MainObject {
        $this->subObject1=$subObject1;
        $this->subObject2=$subObject2;
        $this->theString=$theString;
    }
}

class ObjectCollection
{
    private $map=[];
    public function getObject(SubObject1 $subObject1, SubObject2 $subObject2, string $theString):MainObject {
        if(isset($this->map[$subObject1][$subObject2][$theString])) {
            $mainObject=$this->map[$subObject1][$subObject2][$theString];
        }
        else {
            $mainObject=new MainObject($subObject1, $subObject2, $theString);
            $this->map[$subObject1][$subObject2][$theString]=$mainObject;
        }
        return $mainObject;
    }
}

$objectCollection=new ObjectCollection();
$subObject1_1=new SubObject1();
$subObject1_2=new SubObject1();
$subObject2_1=new SubObject2();
$subObject2_1=new SubObject2();

$o=$objectCollection->getObject($subObject1_1, $subObject2_1, 'hello');    //returns a new object
$o=$objectCollection->getObject($subObject1_2, $subObject2_1, 'hello');    //returns a new object
$o=$objectCollection->getObject($subObject1_1, $subObject2_1, 'goodby');   //returns a new object

$o=$objectCollection->getObject($subObject1_1, $subObject2_1, 'hello');    //returns existing object

How should this be best implemented?

One possibility is something like the following untested code, however, it is a little verbose and am interested if there is a cleaner solution.

public function getObject(SubObject1 $subObject1, SubObject2 $subObject2, string $theString):MainObject {
    if(isset($this->map[$theString])) {
        if($this->map[$theString]->contains($subObject1)) {
            $subObject1Storage=$this->map[$theString][$subObject1];
            if($subObject1Storage->contains($subObject2)) {
                $mainObject=$subObject1Storage[$subObject2];
            }
            else {
                $mainObject=new MainObject($subObject1, $subObject2, $theString);
                $subObject1Storage[$subObject2]=$mainObject;
            }

        }
        else {
            $subObject1Storage = new \SplObjectStorage();
            $this->map[$theString][$subObject1]=$subObject1Storage;
            $mainObject=new MainObject($subObject1, $subObject2, $theString);
            $subObject1Storage[$subObject2]=$mainObject;
        }
    }
    else {
        $this->map[$theString] = new \SplObjectStorage();
        $subObject1Storage = new \SplObjectStorage();
        $this->map[$theString][$subObject1]=$subObject1Storage;
        $mainObject=new MainObject($subObject1, $subObject2, $theString);
        $subObject1Storage[$subObject2]=$mainObject;
    }
    return $mainObject;
}
user1032531
  • 24,767
  • 68
  • 217
  • 387
  • So you're looking for a container combined with a factory of sorts or am I misinterpreting this? – Andrei Sep 04 '18 at 11:39
  • @Andrew Yes, that is correct. I should have included `factory` in the tags, and will add it. – user1032531 Sep 04 '18 at 11:42
  • Why not go the tried and tested route? Assign each object a unique id, dump it into the container, and upon retrieval use "lazy loading" to generate the new object via a factory. So long story short you can pass in any number of unique identifiers and create them on the fly. Hell, you can even do some magic with reflection to simulate a a recursive object creation based on the created object construct arguments. Sure, it's overkill but it's really clean. I suppose you can also use some components(Symfony's DI comes to mind). I can give you some poorly written pseudo code if you want. – Andrei Sep 04 '18 at 11:44
  • @Andrew I think you are on to the right idea. Assign each a unique ID. Pretty much what `SplObjectStorage` does using the hash of the object. Ideally, for my case, it wouldn't be the has of a single object, but that of the two objects plus the string. What do you think about creating a new class who's single method is a constructor which saves the two objects plus the string, and using that object in SplObjectStorage? EDIT. Maybe not, cause they are different instances... – user1032531 Sep 04 '18 at 11:51
  • So you want an immutable hash map basically(or an array which hopefully won't be modified in php world)? But in your case it should be the unique hash of the 2 objects(concatenated I assume) and whatever string you happen to pass in? This seems a bit smelly. Ideally you'd want to keep each object hash separate unless you have a really good reason not to, this allows you to combine whatever hashes you want in the future via whatever means you want. As for the string itself, that's another story and I suppose you can have as many as you want. – Andrei Sep 04 '18 at 11:56
  • Damn comment character limitation. Continuing: you're also sort of misusing `SplObjectStorage` if you concatenate the hashes since they're suppose to be unique(not that `SplObjectStorage` would actually allow you to add 2 of the same hash, but an array wouldn't complain). Also really good question, I wish more of these would pop up :D – Andrei Sep 04 '18 at 11:58
  • I agree it seems a bit smelly. The last method I showed seems right, but gets long. I didn't originally say so, but I also have a need for three objects plus the string, and doing so doubles the length. – user1032531 Sep 04 '18 at 12:03
  • I see what you're saying. I'll post an answer and hopefully you can start from there. Ofc any suggestions/improvements are always more than welcome. We can continue in chat if you wish, chisel things out a little more. – Andrei Sep 04 '18 at 12:06
  • That took a lot longer than I expected. – Andrei Sep 04 '18 at 12:38

1 Answers1

0

The logic I had in mind was as follows:

A factory(or abstract factory in case of too many objects) will take care of creating the object itself.

A container will map unique identifiers with objects created by the factory. And can retrieve objects based on those identifiers.

That's the easy part, the custom part should be even easier, you can add your own methods to do whatever magic you need with aliases and such.

namespace Example;

/**
* Class ObjectFactory
*
* @package Example
*/
class ObjectFactory {

/**
 * This is obviosuly not ideal but it can work
 * with a limited amount of objects. Otherwise use an
 * abstract factory and let each instance take care of a few
 * related objects
 *
 * @param string $objectAlias
 *
 * @throws \Exception
 */
public function make(string $objectAlias) {
  switch($objectAlias) {
    case 'object_unique_id_1':
      try{
        $instance = new $objectAlias;
      }catch (\Exception $exception) {
        // log or whatever and rethrow
        throw new \Exception("Invalid class? maybe, I dunno");
      }
    // return $instance
    // etc
  }
}
}

You can also use Reflection here to recursively get the arguments for the object and dump new instances of the object in the current object based on the arguments in the construct esentially make your own little DI container.

But if you want to keep your sanity use something like Pimple.


Container:

<?php

namespace Example;

/**
 * Class Container
 *
 * @package Example
 */
class Container {

  /**
   * @var array
   */
  private $map = [];

  /**
   * @param $objectAlias
   * @param $objectInstance
   *
   * @throws \Exception
   */
  public function set($objectAlias, $objectInstance) {
    // You can use a try catch here, I chose not to
    if(isset($this->map[$objectAlias])) {
      throw new \Exception("Already exists");
    }
    $this->map[$objectAlias] = $objectInstance;
  }

  /**
   * @param $objectAlias
   *
   * @return bool|mixed
   */
  public function get($objectAlias) {
    if(isset($this->map[$objectAlias])) {
      return $this->map[$objectAlias];
    }
    return false;
  }
}

Specific container which will hold your own methods

<?php

namespace Example;

/**
 * Class ContainerHashMapThingy
 *
 * @package Example
 */
class ContainerHashMapThingy extends Container {
    // Your methods go here
}

And an example object:

<?php

namespace Example;

/**
 * Class ExampleObject1
 *
 * @package Example
 */
class ExampleObject1 {

  /**
   * @return string
   */
  public function alias() {
    // This is just for example sake
    // You can just as well have a config, another class to map them or not map them at all
    return 'example_object_1';
  }
}

And an actual example

<?php

$factory = new \Example\ObjectFactory();
$container = new \Example\Container();

$objectOne = $factory->make('example_object_1');
$container->set('first_object', $objectOne);

The idea here is to give you a clean slate for a container + factory.

If you extend the container you can implement your own methods, remove stuff from the map array, even rewrite the set method to suit your own needs.


While this is not a complete answer it's very hard to give one since, as I said, your needs may vary.

I do hope this gets you on the right track.

Andrei
  • 3,434
  • 5
  • 21
  • 44
  • Thanks Andrew, I don't understand your use of `alias`. Is it determined on the fly using a hash or something? – user1032531 Sep 04 '18 at 13:13
  • Also, I might be misunderstanding your comment `There's no real clean way to do this with unlimited arguments except maybe ...$args but it's more or less the same thing only more verbose`, but if I understand correctly, http://php.net/manual/en/functions.arguments.php#functions.variable-arg-list might help you. – user1032531 Sep 04 '18 at 13:14
  • Ops, sorry about that. I did a little refactoring. Must have left that comment my mistake. – Andrei Sep 04 '18 at 13:18
  • The `alias` part is used to identify each object as a unique identifier. In my `ExampleObject1` i simply used it as a hardcoded value. Not ideal by any means, normally you'd want to have some sort of `yaml` file or whatever strikes your fancy and map each class to it, then parse the yamls and lazy load the classes. But that seems like major overkill. Simply put you can define your own aliases as long as they're unique, you can even continue using the Spl object as a container and simply replace the one in my example. Or even better wrap the Spl in a class and abstract it further. – Andrei Sep 04 '18 at 13:21