Repository: Qiskit/qiskit

Issue:
`'NoneType' object has no attribute 'metadata'` when trying to serialize custom gate on `qiskit-terra==0.21.2` when it used to work on `qiskit-terra==0.21.1`
### Environment

- **Qiskit Terra version**: 0.21.2
- **Python version**: 3.8.13
- **Operating system**: Ubuntu


### What is happening?

Qsikit errors out when trying to serial a circuit which contains a custom gate, when it used to work before on `qiskit-terra==0.21.1`

### How can we reproduce the issue?

Run 

```python
import io
from typing import Optional, Union

import numpy as np
import qiskit
from qiskit.circuit.library import XGate
from qiskit.qpy import dump

class CustomCXGate(qiskit.circuit.ControlledGate):
    def __init__(
        self, label: Optional[str] = None, ctrl_state: Optional[Union[str, int]] = None
    ) -> None:
        super().__init__(
            "cx", 2, [], label, num_ctrl_qubits=1, ctrl_state=ctrl_state, base_gate=XGate()
        )

    def _define(self) -> None:
        qc = qiskit.QuantumCircuit(2, name=self.name)
        qc.cx(0, 1)
        self.definition = qc

    def __array__(self, dtype: Optional[type] = None) -> np.ndarray:
        mat = qiskit.circuit._utils._compute_control_matrix(
            self.base_gate.to_matrix(), self.num_ctrl_qubits, ctrl_state=self.ctrl_state
        )
        return np.asarray(mat, dtype=dtype)

circuit = qiskit.QuantumCircuit(2)
circuit.append(CustomCXGate(),[0, 1])
buf = io.BytesIO()
dump(circuit, buf)
```

This will throw an error

```
circuit.metadata, separators=(",", ":"), cls=metadata_serializer
AttributeError: 'NoneType' object has no attribute 'metadata'

```


### What should happen?

The above snippet should run with no errors.

### Any suggestions?

_No response_


Discussion:
So I took a closer look at this and it was probably #8571 that broke this. To fix handling of open controls we needed to call the inner private `_definition` attribute of `ControlledGate`  objects to ensure when we reconstructed the circuit on deserialization applying the open control was done in the same manner (basically controlled gates use an internal definition and then the `definition` property will adjust for the control state automatically). However in your `CustomCXGate` class you're overloading the definition property directly. So when we go to serialize the custom controlled gate the attribute we're looking for isn't defined and that's causing the error. The only way I can think to workaround this is to try the current way and if it raises catch that and try using `definition`. However, I'm not sure this will do the correct thing if you use an open control, but I'm also not sure if `CustomCXGate` will behave correctly if you use an open control either.
All through the relevant inheritance chain here, `definition` as a setter just writes to `_definition`, so I think the usage as presented should be valid and continue to work (although not strictly be what we expect subclasses of `Instruction` to do).  I think the real problem is a bug in #8571 - now that we _directly_ access `_definition` without looking at `definition` at all, we never trigger the code that calls `_define` if it wasn't already called.

I think the solution is to ensure that `_define` has been called if appropriate (by accessing `definition`), but then continue to _use_ `_definition` like #8571 does.

Resolving patch:
diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py
--- a/qiskit/qpy/binary_io/circuits.py
+++ b/qiskit/qpy/binary_io/circuits.py
@@ -632,6 +632,9 @@ def _write_custom_operation(file_obj, name, operation, custom_operations):
         # state is open, and the definition setter (during a subsequent read) uses the "fully
         # excited" control definition only.
         has_definition = True
+        # Build internal definition to support overloaded subclasses by
+        # calling definition getter on object
+        operation.definition  # pylint: disable=pointless-statement
         data = common.data_to_binary(operation._definition, write_circuit)
         size = len(data)
         num_ctrl_qubits = operation.num_ctrl_qubits


Test patch:
diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py
--- a/test/python/circuit/test_circuit_load_from_qpy.py
+++ b/test/python/circuit/test_circuit_load_from_qpy.py
@@ -34,6 +34,7 @@
 from qiskit.test import QiskitTestCase
 from qiskit.circuit.qpy_serialization import dump, load
 from qiskit.quantum_info.random import random_unitary
+from qiskit.circuit.controlledgate import ControlledGate
 
 
 class TestLoadFromQPY(QiskitTestCase):
@@ -1055,6 +1056,34 @@ def test_standard_control_gates(self):
         new_circuit = load(qpy_file)[0]
         self.assertEqual(qc, new_circuit)
 
+    def test_controlled_gate_subclass_custom_definition(self):
+        """Test controlled gate with overloaded definition.
+
+        Reproduce from: https://github.com/Qiskit/qiskit-terra/issues/8794
+        """
+
+        class CustomCXGate(ControlledGate):
+            """Custom CX with overloaded _define."""
+
+            def __init__(self, label=None, ctrl_state=None):
+                super().__init__(
+                    "cx", 2, [], label, num_ctrl_qubits=1, ctrl_state=ctrl_state, base_gate=XGate()
+                )
+
+            def _define(self) -> None:
+                qc = QuantumCircuit(2, name=self.name)
+                qc.cx(0, 1)
+                self.definition = qc
+
+        qc = QuantumCircuit(2)
+        qc.append(CustomCXGate(), [0, 1])
+        qpy_file = io.BytesIO()
+        dump(qc, qpy_file)
+        qpy_file.seek(0)
+        new_circ = load(qpy_file)[0]
+        self.assertEqual(qc, new_circ)
+        self.assertEqual(qc.decompose(), new_circ.decompose())
+
     def test_load_with_loose_bits(self):
         """Test that loading from a circuit with loose bits works."""
         qc = QuantumCircuit([Qubit(), Qubit(), Clbit()])