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:

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()])