I have a class i have named "AbstractCollider".
It represent ANY colliding object in the world, and my primary motivation for having it was to be able to use a broadphase to find potential collisions.
It contains:
{
ColliderType (sphere, terrain, projectile...)
ColliderID (so its like 5th sphere, 1000244th projectile...)
BodyID (so the broadphase knows where it is)
...
}
Now, when i have 2 of those (potential collision), i look up a collision detector class from a 2D grid based on the types of the 2 colliders. This means i have classes like SphereToSphereCD, SphereToProjectileCD, TerrainToProjectileCD... for each type pair (i reuse the objects when its the same 2 types but different order) all implementing virtual void detectCollidions(Collider&, Collider&)
They then dump the detected collisions to a big list which i then iterate through and apply forces and stuff (collision resolution). The collisions contain information like depth of penetration, collision normal, the colliding bodies ids (the 'abstract' ones), collision location...
That allows having multiple types of objects collide with each other without doing it in O(N^2) (which wouldnt require this layer)
Now, to act on those collisions, you need some kind of "collision callback" system which allows you to add in a handler that is called for collisions. This handler could handle all collisions, or it could handle collisions with a single object (the first needs a single reference in the physics system, the second needs every object to store their own "collision handler" reference) depending on what you need. The more specific ones need more memory and the less specific ones need more checks and branching in the handler since it needs to deal with everything. I would use a CollisionHandler class that again implements a virtual method.
Now, that allows you to know when a collision happens, and which 2 physics bodies it happens between. If the bodies are the same objects as the game objects (as in you have "Player" be a shape that can collide) thats all you need, but in a more flexible system the colliding 'shapes' will only be a part of the entities and you cannot identify the entities from them. So, you need the shapes or the abstractcollider objects to tell what entity owns them. This can be a simple entity ID.